#![cfg_attr(docsrs, feature(doc_cfg))]
mod application;
mod convert_events;
mod event;
pub mod frame_stats;
mod net;
mod window;
#[cfg(feature = "accessibility")]
mod accessibility;
pub use crate::application::BlitzApplication;
pub use crate::event::{BlitzShellEvent, BlitzShellProxy};
pub use crate::frame_stats::{
FrameStatsSnapshot, FrameTimings, TimingStats, clear_frame_stats, latest_frame_stats,
};
#[cfg(feature = "debug-control")]
pub fn set_deep_profiling_permitted(permitted: bool) {
if blitz_traits::profiling::deep_profiling_permitted() == permitted {
return;
}
blitz_traits::profiling::set_deep_profiling_permitted(permitted);
if !permitted {
clear_capture_stores();
}
}
#[cfg(feature = "debug-control")]
#[must_use = "sampling stops as soon as the guard is dropped"]
pub fn begin_deep_profiling() -> Option<DeepProfilingSession> {
let inner = blitz_traits::profiling::begin_deep_profiling()?;
if blitz_traits::profiling::deep_profiling_consumers() == 1 {
clear_capture_stores();
}
Some(DeepProfilingSession { inner: Some(inner) })
}
#[cfg(feature = "debug-control")]
fn clear_capture_stores() {
clear_frame_stats();
blitz_script::script_stats::clear();
}
#[cfg(feature = "debug-control")]
#[derive(Debug)]
pub struct DeepProfilingSession {
inner: Option<blitz_traits::profiling::DeepProfilingGuard>,
}
#[cfg(feature = "debug-control")]
impl Drop for DeepProfilingSession {
fn drop(&mut self) {
drop(self.inner.take());
if blitz_traits::profiling::deep_profiling_consumers() == 0 {
clear_capture_stores();
}
}
}
#[cfg(test)]
pub(crate) static PROFILING_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) fn exclusive_profiling_state() -> std::sync::MutexGuard<'static, ()> {
PROFILING_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(all(test, feature = "debug-control"))]
mod profiling_lifecycle_tests {
use std::time::Duration;
use crate::exclusive_profiling_state as exclusive;
fn record_one_sample_of_each() {
crate::frame_stats::record_frame(
web_time::Instant::now(),
Duration::from_millis(2),
Duration::from_millis(3),
Duration::from_millis(4),
);
blitz_script::script_stats::record_poll(Duration::from_millis(5), true);
}
#[test]
fn a_new_deep_capture_window_drops_all_previous_collector_samples() {
let _serial = exclusive();
crate::set_deep_profiling_permitted(true);
let first = crate::begin_deep_profiling().expect("permitted");
record_one_sample_of_each();
assert!(crate::latest_frame_stats().is_some());
assert!(blitz_script::script_stats::latest_script_stats().is_some());
drop(first);
let _second = crate::begin_deep_profiling().expect("permitted");
assert!(crate::latest_frame_stats().is_none());
assert!(blitz_script::script_stats::latest_script_stats().is_none());
crate::set_deep_profiling_permitted(false);
}
#[test]
fn the_last_consumer_leaving_releases_the_samples() {
let _serial = exclusive();
crate::set_deep_profiling_permitted(true);
let session = crate::begin_deep_profiling().expect("permitted");
record_one_sample_of_each();
assert!(crate::latest_frame_stats().is_some());
drop(session);
assert!(
crate::latest_frame_stats().is_none(),
"dropping the last consumer must release the frame samples",
);
assert!(
blitz_script::script_stats::latest_script_stats().is_none(),
"dropping the last consumer must release the script samples",
);
crate::set_deep_profiling_permitted(false);
}
#[test]
fn permission_without_a_consumer_starts_no_intrusive_collection() {
let _serial = exclusive();
crate::set_deep_profiling_permitted(true);
assert!(
blitz_traits::profiling::deep_profiling_permitted(),
"the owner's switch is on",
);
assert!(
!blitz_traits::profiling::deep_profiling_enabled(),
"but no consumer is attached, so the intrusive collectors stay off",
);
assert_eq!(blitz_traits::profiling::deep_profiling_consumers(), 0);
crate::set_deep_profiling_permitted(false);
}
}
pub use crate::window::{View, WindowConfig};
#[cfg(feature = "data-uri")]
pub use crate::net::DataUriNetProvider;
#[cfg(all(
feature = "file-dialog",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
use blitz_traits::shell::FileDialogFilter;
use blitz_traits::shell::ShellProvider;
use std::sync::Arc;
use winit::cursor::{Cursor, CursorIcon};
use winit::dpi::{LogicalPosition, LogicalSize};
pub use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
pub use winit::window::Window;
use winit::window::{ImeCapabilities, ImeEnableRequest, ImeRequest, ImeRequestData};
#[derive(Default)]
pub struct Config {
pub stylesheets: Vec<String>,
pub base_url: Option<String>,
}
pub fn create_default_event_loop() -> EventLoop {
let mut ev_builder = EventLoop::builder();
#[cfg(target_os = "android")]
{
use winit::platform::android::EventLoopBuilderExtAndroid;
ev_builder.with_android_app(current_android_app());
}
let event_loop = ev_builder.build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop
}
#[cfg(target_os = "android")]
static ANDROID_APP: std::sync::OnceLock<android_activity::AndroidApp> = std::sync::OnceLock::new();
#[cfg(target_os = "android")]
#[cfg_attr(docsrs, doc(cfg(target_os = "android")))]
pub fn set_android_app(app: android_activity::AndroidApp) {
ANDROID_APP.set(app).unwrap()
}
#[cfg(target_os = "android")]
#[cfg_attr(docsrs, doc(cfg(target_os = "android")))]
pub fn current_android_app() -> android_activity::AndroidApp {
ANDROID_APP.get().unwrap().clone()
}
#[cfg(all(
feature = "clipboard",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
static CLIPBOARD: std::sync::OnceLock<Option<std::sync::Mutex<arboard::Clipboard>>> =
std::sync::OnceLock::new();
#[cfg(all(
feature = "clipboard",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
fn with_clipboard<T>(
op: impl FnOnce(&mut arboard::Clipboard) -> Result<T, arboard::Error>,
) -> Result<T, blitz_traits::shell::ClipboardError> {
let cell = CLIPBOARD
.get_or_init(|| match arboard::Clipboard::new() {
Ok(clipboard) => Some(std::sync::Mutex::new(clipboard)),
Err(_) => None,
})
.as_ref()
.ok_or(blitz_traits::shell::ClipboardError)?;
let mut clipboard = cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
op(&mut clipboard).map_err(|_| blitz_traits::shell::ClipboardError)
}
pub struct BlitzShellProvider {
window: Arc<dyn Window>,
proxy: BlitzShellProxy,
}
impl BlitzShellProvider {
pub fn new(window: Arc<dyn Window>, proxy: BlitzShellProxy) -> Self {
Self { window, proxy }
}
}
impl ShellProvider for BlitzShellProvider {
fn request_redraw(&self) {
self.window.request_redraw();
}
fn set_cursor(&self, icon: Option<CursorIcon>) {
match icon {
Some(icon) => {
self.window.set_cursor_visible(true);
self.window.set_cursor(Cursor::Icon(icon));
}
None => {
self.window.set_cursor(Cursor::Icon(CursorIcon::Default));
self.window.set_cursor_visible(false)
}
}
}
fn set_window_title(&self, title: String) {
self.window.set_title(&title);
}
fn set_ime_enabled(&self, is_enabled: bool) {
if is_enabled {
let _ = self.window.request_ime_update(ImeRequest::Enable(
ImeEnableRequest::new(ImeCapabilities::new(), ImeRequestData::default()).unwrap(),
));
} else {
let _ = self.window.request_ime_update(ImeRequest::Disable);
}
}
fn set_ime_cursor_area(&self, x: f32, y: f32, width: f32, height: f32) {
let _ = self.window.request_ime_update(ImeRequest::Update(
ImeRequestData::default().with_cursor_area(
LogicalPosition::new(x, y).into(),
LogicalSize::new(width, height).into(),
),
));
}
fn request_window_close(&self) {
self.proxy.send_event(BlitzShellEvent::CloseWindow {
window_id: self.window.id(),
});
}
fn set_window_minimized(&self, minimized: bool) {
self.window.set_minimized(minimized);
}
fn set_window_maximized(&self, maximized: bool) {
self.window.set_maximized(maximized);
}
fn is_window_maximized(&self) -> bool {
self.window.is_maximized()
}
fn set_window_decorations(&self, decorations: bool) {
self.window.set_decorations(decorations);
}
fn drag_window(&self) {
let _ = self.window.drag_window();
}
#[cfg(all(
feature = "clipboard",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
fn get_clipboard_text(&self) -> Result<String, blitz_traits::shell::ClipboardError> {
with_clipboard(|cb| cb.get_text())
}
#[cfg(all(
feature = "clipboard",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
fn set_clipboard_text(&self, text: String) -> Result<(), blitz_traits::shell::ClipboardError> {
with_clipboard(|cb| cb.set_text(text))
}
#[cfg(all(
feature = "file-dialog",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
fn open_file_dialog(
&self,
multiple: bool,
filter: Option<FileDialogFilter>,
) -> Vec<std::path::PathBuf> {
let mut dialog = rfd::FileDialog::new();
if let Some(FileDialogFilter { name, extensions }) = filter {
dialog = dialog.add_filter(&name, &extensions);
}
let files = if multiple {
dialog.pick_files()
} else {
dialog.pick_file().map(|file| vec![file])
};
files.unwrap_or_default()
}
}
#[cfg(all(
test,
feature = "clipboard",
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
mod clipboard_tests {
use super::with_clipboard;
#[test]
fn an_unavailable_clipboard_is_an_error_and_never_a_panic() {
let unavailable: Option<std::sync::Mutex<arboard::Clipboard>> = None;
let outcome = unavailable
.as_ref()
.ok_or(blitz_traits::shell::ClipboardError)
.map(|_| unreachable!("there is no clipboard to run against"));
assert!(
outcome.is_err(),
"an unopenable clipboard must be reported as an error, not unwrapped",
);
let _ = with_clipboard(|cb| cb.set_text("agencyzero".to_owned()));
let _ = with_clipboard(|cb| cb.get_text());
}
#[test]
fn the_connection_is_opened_at_most_once_and_then_reused() {
for _ in 0..8 {
let _ = with_clipboard(|cb| cb.get_text());
}
assert!(
super::CLIPBOARD.get().is_some(),
"the shared clipboard should be initialised after first use",
);
}
#[test]
fn a_poisoned_lock_does_not_disable_every_later_copy() {
let lock = std::sync::Mutex::new(String::from("still reachable"));
let poisoned = std::panic::catch_unwind(|| {
let _guard = lock.lock().unwrap();
panic!("poison the guard while it is held");
});
assert!(poisoned.is_err(), "the closure above must have panicked");
assert!(lock.is_poisoned(), "the lock must now be poisoned");
let recovered = lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
assert_eq!(*recovered, "still reachable");
drop(recovered);
let _ = std::panic::catch_unwind(|| {
with_clipboard(|_| -> Result<(), arboard::Error> { panic!("poison the shared guard") })
});
let _ = with_clipboard(|cb| cb.get_text());
}
}