use std::path::PathBuf;
use std::sync::Arc;
#[cfg(feature = "application-ipc")]
use crate::ipc::{IpcBridge, IpcHost, IpcOptions, IpcWindow, IpcWindowOwner};
use crate::{
DesktopError, DesktopEvent, DesktopRuntime, DesktopShellConfig, EventRegistrationError,
EventRegistry, EventResponse, EventSubscription, Result, TitlebarStyle, WindowEffect,
WindowHandle, WindowOptions,
};
pub struct DesktopFrame {
#[cfg(feature = "native")]
pub(crate) executor: Arc<crate::execution::ApplicationExecutor>,
pub(crate) app_id: Option<String>,
pub(crate) runtime: Arc<DesktopRuntime>,
pub(crate) window: WindowOptions,
pub(crate) shell: DesktopShellConfig,
pub(crate) events: EventRegistry,
pub(crate) window_handle: WindowHandle,
#[cfg(feature = "application-ipc")]
ipc_owner: IpcWindowOwner,
}
impl DesktopFrame {
#[must_use]
pub fn app_id(&self) -> Option<&str> {
self.app_id.as_deref()
}
#[must_use]
pub fn runtime(&self) -> &Arc<DesktopRuntime> {
&self.runtime
}
#[must_use]
pub fn window(&self) -> &WindowOptions {
&self.window
}
#[must_use]
pub fn shell(&self) -> &DesktopShellConfig {
&self.shell
}
#[must_use]
pub fn events(&self) -> &EventRegistry {
&self.events
}
#[must_use]
pub fn window_handle(&self) -> &WindowHandle {
&self.window_handle
}
pub fn new(runtime: Arc<DesktopRuntime>, window: WindowOptions) -> Result<Self> {
#[cfg(feature = "application-ipc")]
{
Self::with_ipc_options(runtime, window, IpcOptions::default())
}
#[cfg(not(feature = "application-ipc"))]
{
runtime.validate_window(&window)?;
Ok(Self::from_parts(runtime, window))
}
}
#[cfg(feature = "application-ipc")]
pub fn with_ipc_options(
runtime: Arc<DesktopRuntime>,
window: WindowOptions,
options: IpcOptions,
) -> Result<Self> {
runtime.validate_window(&window)?;
let origin = if cfg!(target_os = "windows") {
"https://app.webui.localhost"
} else {
"webui://app"
}
.to_string();
let host = if runtime.is_development() {
IpcHost::Source { origin }
} else {
IpcHost::Packaged { origin }
};
let ipc_owner = IpcWindowOwner::new(runtime.ipc_registry(), options, host)?;
Ok(Self::from_parts(runtime, window, ipc_owner))
}
fn from_parts(
runtime: Arc<DesktopRuntime>,
window: WindowOptions,
#[cfg(feature = "application-ipc")] ipc_owner: IpcWindowOwner,
) -> Self {
let window_handle = WindowHandle::with_background(runtime.live_background());
Self {
#[cfg(feature = "native")]
executor: Arc::default(),
app_id: None,
runtime,
window,
shell: DesktopShellConfig::default(),
events: EventRegistry::default(),
window_handle,
#[cfg(feature = "application-ipc")]
ipc_owner,
}
}
#[must_use]
#[cfg(feature = "application-ipc")]
pub fn ipc(&self) -> IpcWindow {
self.ipc_owner.window()
}
#[must_use]
#[cfg(feature = "application-ipc")]
pub fn ipc_bridge(&self) -> IpcBridge {
self.ipc_owner.bridge()
}
pub fn on_event<F>(&self, handler: F) -> std::result::Result<(), EventRegistrationError>
where
F: Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static,
{
self.events.on_event(handler)
}
pub fn subscribe<F>(
&self,
handler: F,
) -> std::result::Result<EventSubscription, EventRegistrationError>
where
F: Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static,
{
self.events.subscribe(handler)
}
#[must_use]
pub fn with_app_id(mut self, app_id: impl Into<String>) -> Self {
self.app_id = Some(app_id.into());
self
}
#[must_use]
pub fn with_shell(mut self, shell: DesktopShellConfig) -> Self {
self.shell = shell;
self
}
}
impl Drop for DesktopFrame {
fn drop(&mut self) {
#[cfg(feature = "native")]
self.executor.close();
#[cfg(feature = "application-ipc")]
self.ipc_owner.close();
self.window_handle.close();
self.events.close();
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DesktopFrameCapabilities {
pub application_ipc: bool,
pub app_menu: bool,
pub titlebar_styles: bool,
pub supported_effects: &'static [WindowEffect],
pub tray: bool,
pub window_controls: bool,
pub events: bool,
}
impl DesktopFrameCapabilities {
#[must_use]
pub fn supports_effect(&self, effect: WindowEffect) -> bool {
matches!(effect, WindowEffect::None) || self.supported_effects.contains(&effect)
}
}
pub trait DesktopFrameBackend {
#[must_use]
fn capabilities(&self) -> DesktopFrameCapabilities {
DesktopFrameCapabilities::default()
}
fn run_frame(&self, frame: DesktopFrame) -> Result<()>;
}
#[cfg(feature = "native")]
#[derive(Clone, Copy, Debug, Default)]
pub struct PlatformFrameBackend;
#[cfg(feature = "native")]
impl PlatformFrameBackend {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[cfg(feature = "native")]
impl DesktopFrameBackend for PlatformFrameBackend {
fn capabilities(&self) -> DesktopFrameCapabilities {
platform_capabilities()
}
fn run_frame(&self, frame: DesktopFrame) -> Result<()> {
validate_frame(&frame, self.capabilities())?;
platform_run_frame(frame)
}
}
#[cfg(feature = "native")]
pub fn run_runtime(runtime: Arc<DesktopRuntime>, window: WindowOptions) -> Result<()> {
run_frame(DesktopFrame::new(runtime, window)?)
}
#[cfg(feature = "native")]
pub fn run_frame(frame: DesktopFrame) -> Result<()> {
run_frame_with(frame, &PlatformFrameBackend::new())
}
pub fn run_frame_with<B: DesktopFrameBackend + ?Sized>(
frame: DesktopFrame,
backend: &B,
) -> Result<()> {
validate_frame(&frame, backend.capabilities())?;
backend.run_frame(frame)
}
fn validate_frame(frame: &DesktopFrame, capabilities: DesktopFrameCapabilities) -> Result<()> {
frame.runtime.validate_window(&frame.window)?;
validate_frame_capabilities(&frame.window, &frame.shell, capabilities)?;
#[cfg(feature = "application-ipc")]
if frame.runtime.ipc_registry().is_enabled() && !capabilities.application_ipc {
return Err(DesktopError::UnsupportedRuntime {
message: "the selected backend does not support application IPC".to_string(),
help: "use a native WebUI backend or implement the frame-scoped IPC adapter contract"
.to_string(),
});
}
Ok(())
}
pub fn validate_frame_capabilities(
window: &WindowOptions,
shell: &DesktopShellConfig,
capabilities: DesktopFrameCapabilities,
) -> Result<()> {
let unsupported =
if !matches!(window.titlebar, TitlebarStyle::Native) && !capabilities.titlebar_styles {
Some((
"titlebar style",
"Use the native titlebar or a backend that advertises titlebar_styles",
))
} else if !capabilities.supports_effect(window.effect) {
Some((
"window effect",
"Use WindowEffect::None or an effect this backend advertises in supported_effects",
))
} else if shell.tray.is_some() && !capabilities.tray {
Some((
"tray icon",
"Remove shell.tray or select a backend that advertises tray",
))
} else if !shell.menus.is_empty() && !capabilities.app_menu {
Some((
"application menu",
"Remove shell.menus or select a backend that advertises app_menu",
))
} else {
None
};
unsupported.map_or(Ok(()), |(message, help)| {
Err(DesktopError::UnsupportedRuntime {
message: message.to_string(),
help: help.to_string(),
})
})
}
#[must_use]
pub fn find_packaged_resources_dir() -> Option<PathBuf> {
let resources = platform_packaged_resources_dir()?;
resources
.join("manifest.webui-desktop.json")
.is_file()
.then_some(resources)
}
#[cfg(all(feature = "native", target_os = "macos"))]
fn platform_run_frame(frame: DesktopFrame) -> Result<()> {
crate::macos::run_frame(frame).map_err(|source| DesktopError::Backend {
source: source.into(),
})
}
#[cfg(feature = "local-server")]
pub(crate) fn platform_run_local_server_frame(frame: crate::LocalServerFrame) -> Result<()> {
#[cfg(target_os = "macos")]
{
crate::macos::run_local_server_frame(frame).map_err(local_backend_error)
}
#[cfg(target_os = "windows")]
{
crate::windows::run_local_server_frame(frame).map_err(local_backend_error)
}
#[cfg(target_os = "linux")]
{
crate::linux::run_local_server_frame(frame).map_err(local_backend_error)
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
let _ = frame;
Err(DesktopError::UnsupportedRuntime {
message: "local-server native frames are unavailable on this target".to_string(),
help: "Use macOS, Windows or Linux with a native webview runtime".to_string(),
})
}
}
#[cfg(all(
feature = "local-server",
any(target_os = "macos", target_os = "windows", target_os = "linux")
))]
fn local_backend_error(source: anyhow::Error) -> DesktopError {
match source.downcast::<DesktopError>() {
Ok(desktop) => desktop,
Err(source) => DesktopError::Backend {
source: source.into(),
},
}
}
#[cfg(feature = "local-server")]
pub(crate) fn local_platform_capabilities() -> DesktopFrameCapabilities {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
platform_capabilities()
}
#[cfg(target_os = "linux")]
{
DesktopFrameCapabilities {
application_ipc: false,
titlebar_styles: false,
..platform_capabilities()
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
DesktopFrameCapabilities::default()
}
}
#[cfg(all(feature = "native", target_os = "linux"))]
fn platform_run_frame(frame: DesktopFrame) -> Result<()> {
crate::linux::run_frame(frame).map_err(|source| DesktopError::Backend {
source: source.into(),
})
}
#[cfg(all(feature = "native", target_os = "windows"))]
fn platform_run_frame(frame: DesktopFrame) -> Result<()> {
crate::windows::run_frame(frame).map_err(|source| DesktopError::Backend {
source: source.into(),
})
}
#[cfg(all(
feature = "native",
not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
))]
fn platform_run_frame(_frame: DesktopFrame) -> Result<()> {
Err(DesktopError::UnsupportedRuntime {
message: "no native backend for this platform".to_string(),
help: "Use a supported desktop target or supply a DesktopFrameBackend".to_string(),
})
}
#[cfg(all(feature = "native", target_os = "macos"))]
fn platform_capabilities() -> DesktopFrameCapabilities {
DesktopFrameCapabilities {
application_ipc: cfg!(feature = "application-ipc"),
app_menu: true,
titlebar_styles: true,
supported_effects: &[
WindowEffect::Vibrancy,
WindowEffect::Acrylic,
WindowEffect::Mica,
WindowEffect::Tabbed,
],
tray: true,
window_controls: true,
events: true,
}
}
#[cfg(all(feature = "native", target_os = "windows"))]
fn platform_capabilities() -> DesktopFrameCapabilities {
DesktopFrameCapabilities {
application_ipc: cfg!(feature = "application-ipc"),
app_menu: false,
titlebar_styles: true,
supported_effects: &[
WindowEffect::Vibrancy,
WindowEffect::Acrylic,
WindowEffect::Mica,
],
tray: false,
window_controls: true,
events: true,
}
}
#[cfg(all(feature = "native", target_os = "linux"))]
fn platform_capabilities() -> DesktopFrameCapabilities {
DesktopFrameCapabilities {
application_ipc: cfg!(feature = "application-ipc"),
app_menu: false,
titlebar_styles: true,
supported_effects: &[],
tray: false,
window_controls: true,
events: true,
}
}
#[cfg(all(
feature = "native",
not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
))]
fn platform_capabilities() -> DesktopFrameCapabilities {
DesktopFrameCapabilities::default()
}
#[cfg(target_os = "macos")]
fn platform_packaged_resources_dir() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let contents = exe.parent().and_then(std::path::Path::parent)?;
Some(contents.join("Resources").join("webui"))
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn platform_packaged_resources_dir() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
Some(exe.parent()?.join("resources").join("webui"))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn platform_packaged_resources_dir() -> Option<PathBuf> {
None
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
pub(crate) mod test_support {
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use webui_protocol::{FragmentList, WebUIFragment, WebUIProtocol};
use crate::{
BundleIntegrity, DesktopBundleConfig, DesktopBundleManifest, DesktopRuntime,
DesktopShellConfig, WindowOptions,
};
pub(crate) fn bundle() -> (TempDir, DesktopBundleManifest) {
let dir = TempDir::new().unwrap();
std::fs::create_dir(dir.path().join("assets")).unwrap();
WebUIProtocol::new(HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Hello</main>")],
..Default::default()
},
)]))
.to_protobuf_file(dir.path().join("protocol.bin"))
.unwrap();
let manifest = DesktopBundleManifest {
manifest_version: DesktopBundleManifest::VERSION,
app_id: "com.example.webui.frame-test".to_string(),
app_name: "Frame Test".to_string(),
version: "0.0.0".to_string(),
publisher: "Microsoft".to_string(),
entry: "index.html".to_string(),
plugin: None,
protocol_path: PathBuf::from("protocol.bin"),
state_path: None,
assets_dir: PathBuf::from("assets"),
ipc_schema: None,
window: WindowOptions::default(),
shell: DesktopShellConfig::default(),
package_targets: Vec::new(),
integrity: BundleIntegrity::default(),
};
std::fs::write(
dir.path().join("manifest.webui-desktop.json"),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
(dir, manifest)
}
pub(crate) fn runtime() -> (TempDir, Arc<DesktopRuntime>) {
let (dir, manifest) = bundle();
let runtime = DesktopRuntime::from_bundle_config_and_manifest(
DesktopBundleConfig::new(dir.path().to_path_buf()),
manifest,
)
.unwrap();
(dir, Arc::new(runtime))
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use crate::{WindowCommandError, MAX_EVENT_HANDLERS};
#[test]
fn frame_new_uses_default_shell() {
let window = WindowOptions {
title: "Frame Test".to_string(),
..WindowOptions::default()
};
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, window).unwrap();
assert_eq!(frame.window().title, "Frame Test");
assert!(frame.shell().icon_path.is_none());
assert!(frame.shell().menus.is_empty());
assert!(frame.shell().tray.is_none());
assert!(frame.app_id().is_none());
assert!(!frame.runtime().startup_html().is_empty());
let _ = frame.events();
let _ = frame.window_handle();
}
#[test]
fn frame_rejects_window_css_that_disagrees_with_rendered_document() {
let (_bundle, runtime) = test_support::runtime();
let window = WindowOptions {
titlebar: TitlebarStyle::HiddenInset,
..WindowOptions::default()
};
assert!(DesktopFrame::new(runtime, window).is_err());
}
#[cfg(feature = "native")]
#[test]
fn direct_platform_trait_entry_cannot_bypass_capability_validation() {
let (_bundle, runtime) = test_support::runtime();
let mut frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
frame.window.titlebar = TitlebarStyle::HiddenInset;
assert!(PlatformFrameBackend::new().run_frame(frame).is_err());
}
#[test]
fn frame_with_shell_replaces_shell() {
let shell = DesktopShellConfig {
icon_path: Some(PathBuf::from("assets/icon.png")),
tray: Some(crate::TrayConfig {
icon_path: PathBuf::from("assets/tray.png"),
tooltip: None,
}),
..DesktopShellConfig::default()
};
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default())
.unwrap()
.with_shell(shell);
assert_eq!(
frame.shell.icon_path.as_deref(),
Some(std::path::Path::new("assets/icon.png"))
);
assert!(frame.shell.tray.is_some());
}
#[cfg(feature = "native")]
#[test]
fn platform_backend_advertises_only_implemented_capabilities() {
let capabilities = PlatformFrameBackend::new().capabilities();
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{
assert_eq!(
capabilities.application_ipc,
cfg!(feature = "application-ipc")
);
assert!(capabilities.events);
assert!(capabilities.titlebar_styles);
assert!(capabilities.window_controls);
}
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
assert!(capabilities.supports_effect(WindowEffect::None));
#[cfg(target_os = "linux")]
{
assert!(capabilities.supported_effects.is_empty());
assert!(!capabilities.supports_effect(WindowEffect::Mica));
assert!(!capabilities.tray);
}
#[cfg(target_os = "macos")]
{
assert!(capabilities.app_menu);
assert!(capabilities.tray);
assert!(capabilities.supports_effect(WindowEffect::Vibrancy));
assert!(capabilities.supports_effect(WindowEffect::Acrylic));
assert!(capabilities.supports_effect(WindowEffect::Mica));
assert!(capabilities.supports_effect(WindowEffect::Tabbed));
}
#[cfg(target_os = "windows")]
{
assert!(!capabilities.app_menu);
assert!(!capabilities.tray);
assert!(capabilities.supports_effect(WindowEffect::Vibrancy));
assert!(capabilities.supports_effect(WindowEffect::Acrylic));
assert!(capabilities.supports_effect(WindowEffect::Mica));
assert!(!capabilities.supports_effect(WindowEffect::Tabbed));
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
assert_eq!(capabilities, DesktopFrameCapabilities::default());
}
struct RecordingBackend {
calls: AtomicUsize,
fail: bool,
}
impl DesktopFrameBackend for RecordingBackend {
fn run_frame(&self, frame: DesktopFrame) -> Result<()> {
assert_eq!(frame.app_id.as_deref(), Some("com.example.custom"));
assert!(frame.runtime.startup_html().contains("<main>Hello</main>"));
self.calls.fetch_add(1, Ordering::SeqCst);
if self.fail {
Err(DesktopError::Backend {
source: std::io::Error::other("fixture backend failure").into(),
})
} else {
Ok(())
}
}
}
#[test]
fn custom_backend_runs_without_native_features_and_closes_the_frame() {
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default())
.unwrap()
.with_app_id("com.example.custom");
let handle = frame.window_handle.clone();
let backend = RecordingBackend {
calls: AtomicUsize::new(0),
fail: false,
};
run_frame_with(frame, &backend).unwrap();
assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
assert_eq!(handle.request_close(), Err(WindowCommandError::Closed));
}
#[test]
fn capability_rejection_skips_backend_and_closes_the_frame() {
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(
runtime,
WindowOptions {
effect: WindowEffect::Mica,
..WindowOptions::default()
},
)
.unwrap();
let handle = frame.window_handle.clone();
let backend = RecordingBackend {
calls: AtomicUsize::new(0),
fail: false,
};
let error = run_frame_with(frame, &backend).unwrap_err();
assert!(matches!(error, DesktopError::UnsupportedRuntime { .. }));
assert!(error.hint().is_some());
assert_eq!(backend.calls.load(Ordering::SeqCst), 0);
assert_eq!(handle.focus(), Err(WindowCommandError::Closed));
}
#[test]
fn backend_failure_is_propagated_and_closes_the_frame() {
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default())
.unwrap()
.with_app_id("com.example.custom");
let handle = frame.window_handle.clone();
let backend = RecordingBackend {
calls: AtomicUsize::new(0),
fail: true,
};
assert!(matches!(
run_frame_with(frame, &backend),
Err(DesktopError::Backend { .. })
));
assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
assert_eq!(handle.request_close(), Err(WindowCommandError::Closed));
}
#[test]
fn frame_supports_persistent_and_scoped_event_handlers() {
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
let persistent = Arc::new(AtomicUsize::new(0));
let scoped = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&persistent);
frame
.on_event(move |_| {
seen.fetch_add(1, Ordering::SeqCst);
EventResponse::Continue
})
.unwrap();
let seen = Arc::clone(&scoped);
let subscription = frame
.subscribe(move |_| {
seen.fetch_add(1, Ordering::SeqCst);
EventResponse::Continue
})
.unwrap();
let _ = frame.events.dispatch(&DesktopEvent::Ready);
drop(subscription);
let _ = frame.events.dispatch(&DesktopEvent::Ready);
assert_eq!(persistent.load(Ordering::SeqCst), 2);
assert_eq!(scoped.load(Ordering::SeqCst), 1);
}
#[test]
fn frame_propagates_registration_capacity_errors() {
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
for _ in 0..MAX_EVENT_HANDLERS {
frame.on_event(|_| EventResponse::Continue).unwrap();
}
assert_eq!(
frame.on_event(|_| EventResponse::Continue),
Err(EventRegistrationError::Capacity)
);
assert!(matches!(
frame.subscribe(|_| EventResponse::Continue),
Err(EventRegistrationError::Capacity)
));
}
#[test]
fn dropping_frame_releases_cyclic_callbacks_and_closes_retained_handles() {
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
let registry = frame.events.clone();
let handle = frame.window_handle.clone();
let capture = Arc::new(());
let observed = Arc::downgrade(&capture);
let captured_registry = registry.clone();
frame
.on_event(move |_| {
let _ = (&captured_registry, &capture);
EventResponse::Continue
})
.unwrap();
handle.focus().unwrap();
drop(frame);
assert!(observed.upgrade().is_none());
assert!(handle.drain_commands().is_empty());
assert_eq!(handle.request_close(), Err(WindowCommandError::Closed));
assert_eq!(
registry.on_event(|_| EventResponse::Continue),
Err(EventRegistrationError::Closed)
);
assert!(matches!(
registry.subscribe(|_| EventResponse::Continue),
Err(EventRegistrationError::Closed)
));
}
#[test]
fn frame_closes_commands_before_releasing_callback_captures() {
struct SendOnDrop {
handle: WindowHandle,
result: Arc<Mutex<Option<std::result::Result<(), WindowCommandError>>>>,
}
impl Drop for SendOnDrop {
fn drop(&mut self) {
*self.result.lock().unwrap() = Some(self.handle.request_close());
}
}
let (_bundle, runtime) = test_support::runtime();
let frame = DesktopFrame::new(runtime, WindowOptions::default()).unwrap();
let result = Arc::new(Mutex::new(None));
let capture = SendOnDrop {
handle: frame.window_handle.clone(),
result: Arc::clone(&result),
};
frame
.on_event(move |_| {
let _ = &capture;
EventResponse::Continue
})
.unwrap();
drop(frame);
assert_eq!(
*result.lock().unwrap(),
Some(Err(WindowCommandError::Closed))
);
}
}