1use std::path::PathBuf;
5use std::sync::Arc;
6
7#[cfg(feature = "application-ipc")]
8use crate::ipc::{IpcBridge, IpcHost, IpcOptions, IpcWindow, IpcWindowOwner};
9use crate::{
10 DesktopError, DesktopEvent, DesktopRuntime, DesktopShellConfig, EventRegistrationError,
11 EventRegistry, EventResponse, EventSubscription, Result, TitlebarStyle, WindowEffect,
12 WindowHandle, WindowOptions,
13};
14
15pub struct DesktopFrame {
21 #[cfg(feature = "native")]
22 pub(crate) executor: Arc<crate::execution::ApplicationExecutor>,
23 pub(crate) app_id: Option<String>,
25 pub(crate) runtime: Arc<DesktopRuntime>,
27 pub(crate) window: WindowOptions,
29 pub(crate) shell: DesktopShellConfig,
31 pub(crate) events: EventRegistry,
33 pub(crate) window_handle: WindowHandle,
35 #[cfg(feature = "application-ipc")]
36 ipc_owner: IpcWindowOwner,
37}
38
39impl DesktopFrame {
40 #[must_use]
42 pub fn app_id(&self) -> Option<&str> {
43 self.app_id.as_deref()
44 }
45
46 #[must_use]
48 pub fn runtime(&self) -> &Arc<DesktopRuntime> {
49 &self.runtime
50 }
51
52 #[must_use]
54 pub fn window(&self) -> &WindowOptions {
55 &self.window
56 }
57
58 #[must_use]
60 pub fn shell(&self) -> &DesktopShellConfig {
61 &self.shell
62 }
63
64 #[must_use]
66 pub fn events(&self) -> &EventRegistry {
67 &self.events
68 }
69
70 #[must_use]
72 pub fn window_handle(&self) -> &WindowHandle {
73 &self.window_handle
74 }
75
76 pub fn new(runtime: Arc<DesktopRuntime>, window: WindowOptions) -> Result<Self> {
83 #[cfg(feature = "application-ipc")]
84 {
85 Self::with_ipc_options(runtime, window, IpcOptions::default())
86 }
87 #[cfg(not(feature = "application-ipc"))]
88 {
89 runtime.validate_window(&window)?;
90 Ok(Self::from_parts(runtime, window))
91 }
92 }
93
94 #[cfg(feature = "application-ipc")]
101 pub fn with_ipc_options(
102 runtime: Arc<DesktopRuntime>,
103 window: WindowOptions,
104 options: IpcOptions,
105 ) -> Result<Self> {
106 runtime.validate_window(&window)?;
107 let origin = if cfg!(target_os = "windows") {
108 "https://app.webui.localhost"
109 } else {
110 "webui://app"
111 }
112 .to_string();
113 let host = if runtime.is_development() {
114 IpcHost::Source { origin }
115 } else {
116 IpcHost::Packaged { origin }
117 };
118 let ipc_owner = IpcWindowOwner::new(runtime.ipc_registry(), options, host)?;
119 Ok(Self::from_parts(runtime, window, ipc_owner))
120 }
121
122 fn from_parts(
123 runtime: Arc<DesktopRuntime>,
124 window: WindowOptions,
125 #[cfg(feature = "application-ipc")] ipc_owner: IpcWindowOwner,
126 ) -> Self {
127 let window_handle = WindowHandle::with_background(runtime.live_background());
128 Self {
129 #[cfg(feature = "native")]
130 executor: Arc::default(),
131 app_id: None,
132 runtime,
133 window,
134 shell: DesktopShellConfig::default(),
135 events: EventRegistry::default(),
136 window_handle,
137 #[cfg(feature = "application-ipc")]
138 ipc_owner,
139 }
140 }
141
142 #[must_use]
144 #[cfg(feature = "application-ipc")]
145 pub fn ipc(&self) -> IpcWindow {
146 self.ipc_owner.window()
147 }
148
149 #[must_use]
154 #[cfg(feature = "application-ipc")]
155 pub fn ipc_bridge(&self) -> IpcBridge {
156 self.ipc_owner.bridge()
157 }
158
159 pub fn on_event<F>(&self, handler: F) -> std::result::Result<(), EventRegistrationError>
165 where
166 F: Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static,
167 {
168 self.events.on_event(handler)
169 }
170
171 pub fn subscribe<F>(
177 &self,
178 handler: F,
179 ) -> std::result::Result<EventSubscription, EventRegistrationError>
180 where
181 F: Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static,
182 {
183 self.events.subscribe(handler)
184 }
185
186 #[must_use]
188 pub fn with_app_id(mut self, app_id: impl Into<String>) -> Self {
189 self.app_id = Some(app_id.into());
190 self
191 }
192
193 #[must_use]
195 pub fn with_shell(mut self, shell: DesktopShellConfig) -> Self {
196 self.shell = shell;
197 self
198 }
199}
200
201impl Drop for DesktopFrame {
202 fn drop(&mut self) {
203 #[cfg(feature = "native")]
204 self.executor.close();
205 #[cfg(feature = "application-ipc")]
208 self.ipc_owner.close();
209 self.window_handle.close();
210 self.events.close();
211 }
212}
213
214#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
216pub struct DesktopFrameCapabilities {
217 pub application_ipc: bool,
219 pub app_menu: bool,
221 pub titlebar_styles: bool,
223 pub supported_effects: &'static [WindowEffect],
230 pub tray: bool,
232 pub window_controls: bool,
234 pub events: bool,
236}
237
238impl DesktopFrameCapabilities {
239 #[must_use]
243 pub fn supports_effect(&self, effect: WindowEffect) -> bool {
244 matches!(effect, WindowEffect::None) || self.supported_effects.contains(&effect)
245 }
246}
247
248pub trait DesktopFrameBackend {
253 #[must_use]
255 fn capabilities(&self) -> DesktopFrameCapabilities {
256 DesktopFrameCapabilities::default()
257 }
258
259 fn run_frame(&self, frame: DesktopFrame) -> Result<()>;
269}
270
271#[cfg(feature = "native")]
273#[derive(Clone, Copy, Debug, Default)]
274pub struct PlatformFrameBackend;
275
276#[cfg(feature = "native")]
277impl PlatformFrameBackend {
278 #[must_use]
280 pub const fn new() -> Self {
281 Self
282 }
283}
284
285#[cfg(feature = "native")]
286impl DesktopFrameBackend for PlatformFrameBackend {
287 fn capabilities(&self) -> DesktopFrameCapabilities {
288 platform_capabilities()
289 }
290
291 fn run_frame(&self, frame: DesktopFrame) -> Result<()> {
292 validate_frame(&frame, self.capabilities())?;
293 platform_run_frame(frame)
294 }
295}
296
297#[cfg(feature = "native")]
303pub fn run_runtime(runtime: Arc<DesktopRuntime>, window: WindowOptions) -> Result<()> {
304 run_frame(DesktopFrame::new(runtime, window)?)
305}
306
307#[cfg(feature = "native")]
313pub fn run_frame(frame: DesktopFrame) -> Result<()> {
314 run_frame_with(frame, &PlatformFrameBackend::new())
315}
316
317pub fn run_frame_with<B: DesktopFrameBackend + ?Sized>(
328 frame: DesktopFrame,
329 backend: &B,
330) -> Result<()> {
331 validate_frame(&frame, backend.capabilities())?;
332 backend.run_frame(frame)
333}
334
335fn validate_frame(frame: &DesktopFrame, capabilities: DesktopFrameCapabilities) -> Result<()> {
336 frame.runtime.validate_window(&frame.window)?;
337 validate_frame_capabilities(&frame.window, &frame.shell, capabilities)?;
338 #[cfg(feature = "application-ipc")]
339 if frame.runtime.ipc_registry().is_enabled() && !capabilities.application_ipc {
340 return Err(DesktopError::UnsupportedRuntime {
341 message: "the selected backend does not support application IPC".to_string(),
342 help: "use a native WebUI backend or implement the frame-scoped IPC adapter contract"
343 .to_string(),
344 });
345 }
346 Ok(())
347}
348
349pub fn validate_frame_capabilities(
355 window: &WindowOptions,
356 shell: &DesktopShellConfig,
357 capabilities: DesktopFrameCapabilities,
358) -> Result<()> {
359 let unsupported =
360 if !matches!(window.titlebar, TitlebarStyle::Native) && !capabilities.titlebar_styles {
361 Some((
362 "titlebar style",
363 "Use the native titlebar or a backend that advertises titlebar_styles",
364 ))
365 } else if !capabilities.supports_effect(window.effect) {
366 Some((
367 "window effect",
368 "Use WindowEffect::None or an effect this backend advertises in supported_effects",
369 ))
370 } else if shell.tray.is_some() && !capabilities.tray {
371 Some((
372 "tray icon",
373 "Remove shell.tray or select a backend that advertises tray",
374 ))
375 } else if !shell.menus.is_empty() && !capabilities.app_menu {
376 Some((
377 "application menu",
378 "Remove shell.menus or select a backend that advertises app_menu",
379 ))
380 } else {
381 None
382 };
383 unsupported.map_or(Ok(()), |(message, help)| {
384 Err(DesktopError::UnsupportedRuntime {
385 message: message.to_string(),
386 help: help.to_string(),
387 })
388 })
389}
390
391#[must_use]
394pub fn find_packaged_resources_dir() -> Option<PathBuf> {
395 let resources = platform_packaged_resources_dir()?;
396 resources
397 .join("manifest.webui-desktop.json")
398 .is_file()
399 .then_some(resources)
400}
401
402#[cfg(all(feature = "native", target_os = "macos"))]
403fn platform_run_frame(frame: DesktopFrame) -> Result<()> {
404 crate::macos::run_frame(frame).map_err(|source| DesktopError::Backend {
405 source: source.into(),
406 })
407}
408
409#[cfg(feature = "local-server")]
410pub(crate) fn platform_run_local_server_frame(frame: crate::LocalServerFrame) -> Result<()> {
411 #[cfg(target_os = "macos")]
412 {
413 crate::macos::run_local_server_frame(frame).map_err(local_backend_error)
414 }
415 #[cfg(target_os = "windows")]
416 {
417 crate::windows::run_local_server_frame(frame).map_err(local_backend_error)
418 }
419 #[cfg(target_os = "linux")]
420 {
421 crate::linux::run_local_server_frame(frame).map_err(local_backend_error)
422 }
423 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
424 {
425 let _ = frame;
426 Err(DesktopError::UnsupportedRuntime {
427 message: "local-server native frames are unavailable on this target".to_string(),
428 help: "Use macOS, Windows or Linux with a native webview runtime".to_string(),
429 })
430 }
431}
432
433#[cfg(all(
434 feature = "local-server",
435 any(target_os = "macos", target_os = "windows", target_os = "linux")
436))]
437fn local_backend_error(source: anyhow::Error) -> DesktopError {
438 match source.downcast::<DesktopError>() {
439 Ok(desktop) => desktop,
440 Err(source) => DesktopError::Backend {
441 source: source.into(),
442 },
443 }
444}
445
446#[cfg(feature = "local-server")]
447pub(crate) fn local_platform_capabilities() -> DesktopFrameCapabilities {
448 #[cfg(any(target_os = "macos", target_os = "windows"))]
449 {
450 platform_capabilities()
451 }
452 #[cfg(target_os = "linux")]
453 {
454 DesktopFrameCapabilities {
455 application_ipc: false,
456 titlebar_styles: false,
457 ..platform_capabilities()
458 }
459 }
460 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
461 {
462 DesktopFrameCapabilities::default()
463 }
464}
465
466#[cfg(all(feature = "native", target_os = "linux"))]
467fn platform_run_frame(frame: DesktopFrame) -> Result<()> {
468 crate::linux::run_frame(frame).map_err(|source| DesktopError::Backend {
469 source: source.into(),
470 })
471}
472
473#[cfg(all(feature = "native", target_os = "windows"))]
474fn platform_run_frame(frame: DesktopFrame) -> Result<()> {
475 crate::windows::run_frame(frame).map_err(|source| DesktopError::Backend {
476 source: source.into(),
477 })
478}
479
480#[cfg(all(
481 feature = "native",
482 not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
483))]
484fn platform_run_frame(_frame: DesktopFrame) -> Result<()> {
485 Err(DesktopError::UnsupportedRuntime {
486 message: "no native backend for this platform".to_string(),
487 help: "Use a supported desktop target or supply a DesktopFrameBackend".to_string(),
488 })
489}
490
491#[cfg(all(feature = "native", target_os = "macos"))]
497fn platform_capabilities() -> DesktopFrameCapabilities {
498 DesktopFrameCapabilities {
499 application_ipc: cfg!(feature = "application-ipc"),
500 app_menu: true,
501 titlebar_styles: true,
502 supported_effects: &[
503 WindowEffect::Vibrancy,
504 WindowEffect::Acrylic,
505 WindowEffect::Mica,
506 WindowEffect::Tabbed,
507 ],
508 tray: true,
509 window_controls: true,
510 events: true,
511 }
512}
513
514#[cfg(all(feature = "native", target_os = "windows"))]
521fn platform_capabilities() -> DesktopFrameCapabilities {
522 DesktopFrameCapabilities {
523 application_ipc: cfg!(feature = "application-ipc"),
524 app_menu: false,
525 titlebar_styles: true,
526 supported_effects: &[
527 WindowEffect::Vibrancy,
528 WindowEffect::Acrylic,
529 WindowEffect::Mica,
530 ],
531 tray: false,
532 window_controls: true,
533 events: true,
534 }
535}
536
537#[cfg(all(feature = "native", target_os = "linux"))]
542fn platform_capabilities() -> DesktopFrameCapabilities {
543 DesktopFrameCapabilities {
544 application_ipc: cfg!(feature = "application-ipc"),
545 app_menu: false,
546 titlebar_styles: true,
547 supported_effects: &[],
548 tray: false,
549 window_controls: true,
550 events: true,
551 }
552}
553
554#[cfg(all(
555 feature = "native",
556 not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
557))]
558fn platform_capabilities() -> DesktopFrameCapabilities {
559 DesktopFrameCapabilities::default()
560}
561
562#[cfg(target_os = "macos")]
563fn platform_packaged_resources_dir() -> Option<PathBuf> {
564 let exe = std::env::current_exe().ok()?;
565 let contents = exe.parent().and_then(std::path::Path::parent)?;
566 Some(contents.join("Resources").join("webui"))
567}
568
569#[cfg(any(target_os = "windows", target_os = "linux"))]
570fn platform_packaged_resources_dir() -> Option<PathBuf> {
571 let exe = std::env::current_exe().ok()?;
572 Some(exe.parent()?.join("resources").join("webui"))
573}
574
575#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
576fn platform_packaged_resources_dir() -> Option<PathBuf> {
577 None
578}
579
580#[cfg(test)]
581#[allow(clippy::disallowed_methods)]
582pub(crate) mod test_support {
583 use std::collections::HashMap;
584 use std::path::PathBuf;
585 use std::sync::Arc;
586 use tempfile::TempDir;
587 use webui_protocol::{FragmentList, WebUIFragment, WebUIProtocol};
588
589 use crate::{
590 BundleIntegrity, DesktopBundleConfig, DesktopBundleManifest, DesktopRuntime,
591 DesktopShellConfig, WindowOptions,
592 };
593
594 pub(crate) fn bundle() -> (TempDir, DesktopBundleManifest) {
595 let dir = TempDir::new().unwrap();
596 std::fs::create_dir(dir.path().join("assets")).unwrap();
597 WebUIProtocol::new(HashMap::from([(
599 "index.html".to_string(),
600 FragmentList {
601 fragments: vec![WebUIFragment::raw("<main>Hello</main>")],
602 ..Default::default()
603 },
604 )]))
605 .to_protobuf_file(dir.path().join("protocol.bin"))
606 .unwrap();
607 let manifest = DesktopBundleManifest {
608 manifest_version: DesktopBundleManifest::VERSION,
609 app_id: "com.example.webui.frame-test".to_string(),
610 app_name: "Frame Test".to_string(),
611 version: "0.0.0".to_string(),
612 publisher: "Microsoft".to_string(),
613 entry: "index.html".to_string(),
614 plugin: None,
615 protocol_path: PathBuf::from("protocol.bin"),
616 state_path: None,
617 assets_dir: PathBuf::from("assets"),
618 ipc_schema: None,
619 window: WindowOptions::default(),
620 shell: DesktopShellConfig::default(),
621 package_targets: Vec::new(),
622 integrity: BundleIntegrity::default(),
623 };
624 std::fs::write(
625 dir.path().join("manifest.webui-desktop.json"),
626 serde_json::to_vec(&manifest).unwrap(),
627 )
628 .unwrap();
629 (dir, manifest)
630 }
631
632 pub(crate) fn runtime() -> (TempDir, Arc<DesktopRuntime>) {
633 let (dir, manifest) = bundle();
634 let runtime = DesktopRuntime::from_bundle_config_and_manifest(
635 DesktopBundleConfig::new(dir.path().to_path_buf()),
636 manifest,
637 )
638 .unwrap();
639 (dir, Arc::new(runtime))
640 }
641}
642
643#[cfg(test)]
644#[allow(clippy::disallowed_methods)]
645mod tests {
646 use super::*;
647 use std::sync::atomic::{AtomicUsize, Ordering};
648 use std::sync::Mutex;
649
650 use crate::{WindowCommandError, MAX_EVENT_HANDLERS};
651
652 #[test]
653 fn frame_new_uses_default_shell() {
654 let window = WindowOptions {
655 title: "Frame Test".to_string(),
656 ..WindowOptions::default()
657 };
658
659 let (_bundle, runtime) = test_support::runtime();
660 let frame = DesktopFrame::new(runtime, window).unwrap();
661
662 assert_eq!(frame.window().title, "Frame Test");
663 assert!(frame.shell().icon_path.is_none());
664 assert!(frame.shell().menus.is_empty());
665 assert!(frame.shell().tray.is_none());
666 assert!(frame.app_id().is_none());
667 assert!(!frame.runtime().startup_html().is_empty());
668 let _ = frame.events();
669 let _ = frame.window_handle();
670 }
671
672 #[test]
673 fn frame_rejects_window_css_that_disagrees_with_rendered_document() {
674 let (_bundle, runtime) = test_support::runtime();
675 let window = WindowOptions {
676 titlebar: TitlebarStyle::HiddenInset,
677 ..WindowOptions::default()
678 };
679 assert!(DesktopFrame::new(runtime, window).is_err());
680 }
681
682 #[cfg(feature = "native")]
683 #[test]
684 fn direct_platform_trait_entry_cannot_bypass_capability_validation() {
685 let (_bundle, runtime) = test_support::runtime();
686 let mut frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
687 frame.window.titlebar = TitlebarStyle::HiddenInset;
690 assert!(PlatformFrameBackend::new().run_frame(frame).is_err());
691 }
692
693 #[test]
694 fn frame_with_shell_replaces_shell() {
695 let shell = DesktopShellConfig {
696 icon_path: Some(PathBuf::from("assets/icon.png")),
697 tray: Some(crate::TrayConfig {
698 icon_path: PathBuf::from("assets/tray.png"),
699 tooltip: None,
700 }),
701 ..DesktopShellConfig::default()
702 };
703
704 let (_bundle, runtime) = test_support::runtime();
705 let frame = DesktopFrame::new(runtime, WindowOptions::default())
706 .unwrap()
707 .with_shell(shell);
708
709 assert_eq!(
710 frame.shell.icon_path.as_deref(),
711 Some(std::path::Path::new("assets/icon.png"))
712 );
713 assert!(frame.shell.tray.is_some());
714 }
715
716 #[cfg(feature = "native")]
717 #[test]
718 fn platform_backend_advertises_only_implemented_capabilities() {
719 let capabilities = PlatformFrameBackend::new().capabilities();
720
721 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
726 {
727 assert_eq!(
728 capabilities.application_ipc,
729 cfg!(feature = "application-ipc")
730 );
731 assert!(capabilities.events);
732 assert!(capabilities.titlebar_styles);
733 assert!(capabilities.window_controls);
734 }
735 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
737 assert!(capabilities.supports_effect(WindowEffect::None));
738 #[cfg(target_os = "linux")]
740 {
741 assert!(capabilities.supported_effects.is_empty());
742 assert!(!capabilities.supports_effect(WindowEffect::Mica));
743 assert!(!capabilities.tray);
744 }
745 #[cfg(target_os = "macos")]
747 {
748 assert!(capabilities.app_menu);
749 assert!(capabilities.tray);
750 assert!(capabilities.supports_effect(WindowEffect::Vibrancy));
752 assert!(capabilities.supports_effect(WindowEffect::Acrylic));
753 assert!(capabilities.supports_effect(WindowEffect::Mica));
754 assert!(capabilities.supports_effect(WindowEffect::Tabbed));
755 }
756 #[cfg(target_os = "windows")]
757 {
758 assert!(!capabilities.app_menu);
759 assert!(!capabilities.tray);
760 assert!(capabilities.supports_effect(WindowEffect::Vibrancy));
762 assert!(capabilities.supports_effect(WindowEffect::Acrylic));
763 assert!(capabilities.supports_effect(WindowEffect::Mica));
764 assert!(!capabilities.supports_effect(WindowEffect::Tabbed));
765 }
766 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
768 assert_eq!(capabilities, DesktopFrameCapabilities::default());
769 }
770
771 struct RecordingBackend {
772 calls: AtomicUsize,
773 fail: bool,
774 }
775
776 impl DesktopFrameBackend for RecordingBackend {
777 fn run_frame(&self, frame: DesktopFrame) -> Result<()> {
778 assert_eq!(frame.app_id.as_deref(), Some("com.example.custom"));
779 assert!(frame.runtime.startup_html().contains("<main>Hello</main>"));
780 self.calls.fetch_add(1, Ordering::SeqCst);
781 if self.fail {
782 Err(DesktopError::Backend {
783 source: std::io::Error::other("fixture backend failure").into(),
784 })
785 } else {
786 Ok(())
787 }
788 }
789 }
790
791 #[test]
792 fn custom_backend_runs_without_native_features_and_closes_the_frame() {
793 let (_bundle, runtime) = test_support::runtime();
794 let frame = DesktopFrame::new(runtime, WindowOptions::default())
795 .unwrap()
796 .with_app_id("com.example.custom");
797 let handle = frame.window_handle.clone();
798 let backend = RecordingBackend {
799 calls: AtomicUsize::new(0),
800 fail: false,
801 };
802
803 run_frame_with(frame, &backend).unwrap();
804
805 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
806 assert_eq!(handle.request_close(), Err(WindowCommandError::Closed));
807 }
808
809 #[test]
810 fn capability_rejection_skips_backend_and_closes_the_frame() {
811 let (_bundle, runtime) = test_support::runtime();
812 let frame = DesktopFrame::new(
813 runtime,
814 WindowOptions {
815 effect: WindowEffect::Mica,
816 ..WindowOptions::default()
817 },
818 )
819 .unwrap();
820 let handle = frame.window_handle.clone();
821 let backend = RecordingBackend {
822 calls: AtomicUsize::new(0),
823 fail: false,
824 };
825
826 let error = run_frame_with(frame, &backend).unwrap_err();
827
828 assert!(matches!(error, DesktopError::UnsupportedRuntime { .. }));
829 assert!(error.hint().is_some());
830 assert_eq!(backend.calls.load(Ordering::SeqCst), 0);
831 assert_eq!(handle.focus(), Err(WindowCommandError::Closed));
832 }
833
834 #[test]
835 fn backend_failure_is_propagated_and_closes_the_frame() {
836 let (_bundle, runtime) = test_support::runtime();
837 let frame = DesktopFrame::new(runtime, WindowOptions::default())
838 .unwrap()
839 .with_app_id("com.example.custom");
840 let handle = frame.window_handle.clone();
841 let backend = RecordingBackend {
842 calls: AtomicUsize::new(0),
843 fail: true,
844 };
845
846 assert!(matches!(
847 run_frame_with(frame, &backend),
848 Err(DesktopError::Backend { .. })
849 ));
850 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
851 assert_eq!(handle.request_close(), Err(WindowCommandError::Closed));
852 }
853
854 #[test]
855 fn frame_supports_persistent_and_scoped_event_handlers() {
856 let (_bundle, runtime) = test_support::runtime();
857 let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
858 let persistent = Arc::new(AtomicUsize::new(0));
859 let scoped = Arc::new(AtomicUsize::new(0));
860 let seen = Arc::clone(&persistent);
861 frame
862 .on_event(move |_| {
863 seen.fetch_add(1, Ordering::SeqCst);
864 EventResponse::Continue
865 })
866 .unwrap();
867 let seen = Arc::clone(&scoped);
868 let subscription = frame
869 .subscribe(move |_| {
870 seen.fetch_add(1, Ordering::SeqCst);
871 EventResponse::Continue
872 })
873 .unwrap();
874
875 let _ = frame.events.dispatch(&DesktopEvent::Ready);
876 drop(subscription);
877 let _ = frame.events.dispatch(&DesktopEvent::Ready);
878
879 assert_eq!(persistent.load(Ordering::SeqCst), 2);
880 assert_eq!(scoped.load(Ordering::SeqCst), 1);
881 }
882
883 #[test]
884 fn frame_propagates_registration_capacity_errors() {
885 let (_bundle, runtime) = test_support::runtime();
886 let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
887 for _ in 0..MAX_EVENT_HANDLERS {
888 frame.on_event(|_| EventResponse::Continue).unwrap();
889 }
890
891 assert_eq!(
892 frame.on_event(|_| EventResponse::Continue),
893 Err(EventRegistrationError::Capacity)
894 );
895 assert!(matches!(
896 frame.subscribe(|_| EventResponse::Continue),
897 Err(EventRegistrationError::Capacity)
898 ));
899 }
900
901 #[test]
902 fn dropping_frame_releases_cyclic_callbacks_and_closes_retained_handles() {
903 let (_bundle, runtime) = test_support::runtime();
904 let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
905 let registry = frame.events.clone();
906 let handle = frame.window_handle.clone();
907 let capture = Arc::new(());
908 let observed = Arc::downgrade(&capture);
909 let captured_registry = registry.clone();
910 frame
911 .on_event(move |_| {
912 let _ = (&captured_registry, &capture);
913 EventResponse::Continue
914 })
915 .unwrap();
916 handle.focus().unwrap();
917
918 drop(frame);
919
920 assert!(observed.upgrade().is_none());
921 assert!(handle.drain_commands().is_empty());
922 assert_eq!(handle.request_close(), Err(WindowCommandError::Closed));
923 assert_eq!(
924 registry.on_event(|_| EventResponse::Continue),
925 Err(EventRegistrationError::Closed)
926 );
927 assert!(matches!(
928 registry.subscribe(|_| EventResponse::Continue),
929 Err(EventRegistrationError::Closed)
930 ));
931 }
932
933 #[test]
934 fn frame_closes_commands_before_releasing_callback_captures() {
935 struct SendOnDrop {
936 handle: WindowHandle,
937 result: Arc<Mutex<Option<std::result::Result<(), WindowCommandError>>>>,
938 }
939 impl Drop for SendOnDrop {
940 fn drop(&mut self) {
941 *self.result.lock().unwrap() = Some(self.handle.request_close());
942 }
943 }
944
945 let (_bundle, runtime) = test_support::runtime();
946 let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
947 let result = Arc::new(Mutex::new(None));
948 let capture = SendOnDrop {
949 handle: frame.window_handle.clone(),
950 result: Arc::clone(&result),
951 };
952 frame
953 .on_event(move |_| {
954 let _ = &capture;
955 EventResponse::Continue
956 })
957 .unwrap();
958
959 drop(frame);
960
961 assert_eq!(
962 *result.lock().unwrap(),
963 Some(Err(WindowCommandError::Closed))
964 );
965 }
966}