1#![allow(missing_docs)]
3
4use std::{
5 any::Any,
6 fmt,
7 sync::{Arc, atomic::Ordering},
8};
9
10use egui::Context;
11
12use crate::{
13 actions::InputAction,
14 fixtures::FixtureHandler,
15 instrument::{ACTIVE, swallow_panic},
16 registry::Inner,
17 types::FixtureSpec,
18};
19
20#[derive(Clone, Debug, Default)]
21enum DevMcpState {
22 #[default]
23 Inactive,
24 Active(Arc<Inner>),
25}
26
27pub trait RuntimeHooks: Send + Sync {
28 fn as_any(&self) -> &(dyn Any + Send + Sync);
29
30 fn on_raw_input(&self, _inner: &Inner, _events: &[egui::Event]) {}
31
32 fn on_frame_end(&self, _inner: &Inner, _ctx: &Context) {}
33}
34
35#[derive(Clone, Default)]
37pub struct DevMcp {
38 state: DevMcpState,
39 fixtures: Vec<FixtureSpec>,
40 verbose_logging: bool,
41 fixture_handler: Option<FixtureHandler>,
42}
43
44impl fmt::Debug for DevMcp {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.debug_struct("DevMcp")
47 .field("state", &self.state)
48 .field("fixtures", &self.fixtures)
49 .field("verbose_logging", &self.verbose_logging)
50 .finish()
51 }
52}
53
54impl DevMcp {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn verbose_logging(mut self, verbose_logging: bool) -> Self {
62 self.verbose_logging = verbose_logging;
63 if let Some(inner) = self.inner() {
64 inner.set_verbose_logging(verbose_logging);
65 }
66 self
67 }
68
69 pub fn fixtures(mut self, fixtures: impl IntoIterator<Item = FixtureSpec>) -> Self {
71 self.fixtures = fixtures.into_iter().collect();
72 if let Some(inner) = self.inner() {
73 inner.fixtures.set_fixtures(self.fixtures.clone());
74 }
75 self
76 }
77
78 pub fn on_fixture<F>(mut self, handler: F) -> Self
83 where
84 F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
85 {
86 let handler: FixtureHandler = Arc::new(handler);
87 if let Some(inner) = self.inner() {
88 inner.fixtures.set_fixture_handler(handler.clone());
89 }
90 self.fixture_handler = Some(handler);
91 self
92 }
93
94 pub fn is_enabled(&self) -> bool {
96 matches!(self.state, DevMcpState::Active(_))
97 }
98
99 #[doc(hidden)]
100 pub fn inner_arc(&self) -> Option<Arc<Inner>> {
101 self.inner().map(Arc::clone)
102 }
103
104 #[doc(hidden)]
105 pub fn runtime_hooks(&self) -> Option<Arc<dyn RuntimeHooks>> {
106 self.inner().and_then(|inner| inner.runtime_hooks())
107 }
108
109 fn verbose_logging_enabled(&self) -> bool {
110 self.inner()
111 .map_or(self.verbose_logging, |inner| inner.verbose_logging())
112 }
113
114 #[cfg(test)]
115 fn context_for(&self, viewport_id: egui::ViewportId) -> Option<Context> {
116 self.inner()
117 .and_then(|inner| inner.context_for(viewport_id))
118 }
119
120 #[doc(hidden)]
121 pub fn activate_runtime(mut self, inner: Arc<Inner>, hooks: Arc<dyn RuntimeHooks>) -> Self {
122 inner.set_runtime_hooks(hooks);
123 inner.set_verbose_logging(self.verbose_logging);
124 if !self.fixtures.is_empty() {
125 inner.fixtures.set_fixtures(self.fixtures.clone());
126 }
127 if let Some(handler) = &self.fixture_handler {
128 inner.fixtures.set_fixture_handler(handler.clone());
129 }
130 self.state = DevMcpState::Active(inner);
131 self
132 }
133
134 pub(crate) fn inner(&self) -> Option<&Arc<Inner>> {
135 match &self.state {
136 DevMcpState::Inactive => None,
137 DevMcpState::Active(inner) => Some(inner),
138 }
139 }
140
141 pub(crate) fn begin_frame(&self, ctx: &Context) {
145 let Some(inner) = self.inner() else {
146 return;
147 };
148 swallow_panic("begin_frame", || {
149 let viewport_id = ctx.viewport_id();
150 inner.capture_context(viewport_id, ctx);
151 inner.widgets.clear_registry(viewport_id);
152 ACTIVE.with(|active| {
153 if let Ok(mut active) = active.try_borrow_mut() {
154 *active = Some(Arc::clone(inner));
155 } else {
156 eprintln!("eguidev: begin_frame skipped; active already borrowed");
157 }
158 });
159 });
160 }
161
162 pub(crate) fn end_frame(&self, ctx: &Context) {
166 let Some(inner) = self.inner() else {
167 return;
168 };
169 swallow_panic("end_frame", || {
170 self.finish_frame(inner, ctx);
171 ACTIVE.with(|active| {
172 if let Ok(mut active) = active.try_borrow_mut() {
173 *active = None;
174 } else {
175 eprintln!("eguidev: end_frame skipped; active already borrowed");
176 }
177 });
178 });
179 }
180
181 fn finish_frame(&self, inner: &Arc<Inner>, ctx: &Context) {
182 inner.widgets.finalize_registry(ctx.viewport_id());
183 let next_frame = inner.frame_count() + 1;
184 inner
185 .viewports
186 .capture_input_snapshot(ctx, inner.fixture_epoch(), next_frame);
187 inner.advance_frame();
188 if let Some(hooks) = inner.runtime_hooks() {
189 hooks.on_frame_end(inner, ctx);
190 }
191 }
192
193 pub(crate) fn raw_input_hook(&self, ctx: &Context, raw_input: &mut egui::RawInput) {
195 let Some(inner) = self.inner() else {
196 return;
197 };
198 swallow_panic("raw_input_hook", || {
199 let viewport_id = raw_input.viewport_id;
200 inner.capture_context(viewport_id, ctx);
201 if let Some(hooks) = inner.runtime_hooks() {
202 hooks.on_raw_input(inner, &raw_input.events);
203 }
204 let actions = inner.actions.drain_actions(viewport_id);
205 if !actions.is_empty() {
206 inner
207 .last_action_frame
208 .store(inner.frame_count(), Ordering::Relaxed);
209 if self.verbose_logging_enabled() {
210 eprintln!(
211 "eguidev: raw_input_hook viewport={:?} actions={}",
212 viewport_id,
213 actions.len()
214 );
215 }
216 }
217 let base_modifiers = raw_input.modifiers;
218 let mut current_modifiers = base_modifiers;
219 let mut force_focus = false;
220 for action in &actions {
221 if let InputAction::Key {
222 pressed, modifiers, ..
223 } = action
224 {
225 current_modifiers = if *pressed {
226 base_modifiers.plus((*modifiers).into())
227 } else {
228 base_modifiers
229 };
230 }
231 if matches!(
232 action,
233 InputAction::Key { .. } | InputAction::Text { .. } | InputAction::Paste { .. }
234 ) {
235 force_focus = true;
236 }
237 }
238 raw_input.modifiers = current_modifiers;
239 if force_focus {
240 raw_input.focused = true;
241 }
242 for action in actions {
243 action.apply(raw_input);
244 }
245 });
246 }
247}
248
249#[must_use = "FrameGuard must be held for the duration of the frame"]
251pub struct FrameGuard<'a> {
252 devmcp: &'a DevMcp,
254 ctx: &'a egui::Context,
256}
257
258impl<'a> FrameGuard<'a> {
259 pub fn new(devmcp: &'a DevMcp, ctx: &'a Context) -> Self {
261 devmcp.begin_frame(ctx);
262 Self { devmcp, ctx }
263 }
264}
265
266impl Drop for FrameGuard<'_> {
267 fn drop(&mut self) {
268 self.devmcp.end_frame(self.ctx);
269 }
270}
271
272pub fn raw_input_hook(devmcp: &DevMcp, ctx: &Context, raw_input: &mut egui::RawInput) {
274 devmcp.raw_input_hook(ctx, raw_input);
275}
276
277#[cfg(test)]
278#[allow(deprecated)]
279#[allow(clippy::tests_outside_test_module)]
280mod inactive_tests {
281 use egui::Context;
282
283 use super::*;
284 use crate::{instrument, ui_ext::DevUiExt};
285
286 #[test]
287 fn inactive_raw_input_hook_is_a_noop() {
288 let devmcp = DevMcp::new();
289 let ctx = Context::default();
290 let mut raw_input = egui::RawInput {
291 viewport_id: egui::ViewportId::ROOT,
292 focused: false,
293 ..Default::default()
294 };
295
296 devmcp.raw_input_hook(&ctx, &mut raw_input);
297
298 assert!(!raw_input.focused);
299 assert!(raw_input.events.is_empty());
300 }
301
302 #[test]
303 fn inactive_frame_guard_does_not_capture_context() {
304 let devmcp = DevMcp::new();
305 let ctx = Context::default();
306 instrument::reset_test_counters();
307
308 let _output = ctx.run_ui(egui::RawInput::default(), |ui| {
309 let ctx = ui.ctx().clone();
310 let _guard = FrameGuard::new(&devmcp, &ctx);
311 ui.dev_button("inactive.button", "Inactive");
312 });
313
314 assert!(devmcp.context_for(egui::ViewportId::ROOT).is_none());
315 assert_eq!(instrument::test_layout_capture_count(), 0);
316 }
317}