#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
use cranpose_app_shell::FramePacingMode;
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
use std::path::PathBuf;
#[cfg(all(feature = "renderer-wgpu", any(feature = "desktop", feature = "ios")))]
use thiserror::Error;
pub struct AppSettings {
pub window_title: String,
pub initial_width: u32,
pub initial_height: u32,
pub initial_size_explicit: bool,
pub fonts: Option<&'static [&'static [u8]]>,
pub android_use_system_fonts: bool,
pub android_overlay_window: Option<AndroidOverlayWindowOptions>,
pub headless: bool,
pub primary_window_visible: bool,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub dev_options: cranpose_app_shell::DevOptions,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub frame_pacing_mode: FramePacingMode,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub frame_pacing_explicit: bool,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
pub test_driver: Option<Box<dyn FnOnce(crate::desktop::Robot) + Send + 'static>>,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
pub robot_app_hook: Option<Box<crate::RobotAppHook>>,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub record_to: Option<PathBuf>,
}
impl Default for AppSettings {
fn default() -> Self {
Self {
window_title: "Compose App".into(),
initial_width: 800,
initial_height: 600,
initial_size_explicit: false,
fonts: None,
android_use_system_fonts: false,
android_overlay_window: None,
headless: false,
primary_window_visible: true,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
dev_options: cranpose_app_shell::DevOptions::default(),
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
frame_pacing_mode: FramePacingMode::Vsync,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
frame_pacing_explicit: false,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
test_driver: None,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
robot_app_hook: None,
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
record_to: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AndroidOverlayWindowOptions {
pub width: u32,
pub height: u32,
pub x: i32,
pub y: i32,
pub focusable: bool,
}
impl AndroidOverlayWindowOptions {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
x: 0,
y: 0,
focusable: false,
}
}
pub fn with_position(mut self, x: i32, y: i32) -> Self {
self.x = x;
self.y = y;
self
}
pub fn with_focusable(mut self, focusable: bool) -> Self {
self.focusable = focusable;
self
}
pub fn is_valid(self) -> bool {
self.width > 0 && self.height > 0
}
}
#[cfg(all(feature = "renderer-wgpu", any(feature = "desktop", feature = "ios")))]
#[derive(Debug, Error)]
pub enum LaunchError {
#[error("failed to create desktop event loop: {0}")]
EventLoopCreate(#[source] winit::error::EventLoopError),
#[error("failed to create desktop window: {0}")]
WindowCreate(#[source] winit::error::RequestError),
#[error("failed to create desktop rendering surface: {0}")]
SurfaceCreate(#[source] wgpu::CreateSurfaceError),
#[error("desktop rendering surface reports no supported formats")]
NoSurfaceFormat,
#[error("desktop rendering surface reports no supported alpha modes")]
NoSurfaceAlphaMode,
#[error("no compatible GPU adapter was available: {0}")]
NoAdapter(#[source] wgpu::RequestAdapterError),
#[error("failed to create GPU device: {0}")]
DeviceCreate(#[source] wgpu::RequestDeviceError),
#[error("desktop GPU context is not initialized")]
GpuContextUnavailable,
#[error("desktop application content is unavailable during launch")]
ContentUnavailable,
#[error("desktop event loop terminated with error: {0}")]
EventLoopRun(#[source] winit::error::EventLoopError),
#[cfg(feature = "robot")]
#[error("desktop robot test driver panicked: {0}")]
TestDriverPanic(String),
}
#[cfg(all(feature = "renderer-wgpu", any(feature = "desktop", feature = "ios")))]
pub(crate) fn exit_after_launch_error(context: &str, error: LaunchError) -> ! {
eprintln!("{context}: {error}");
std::process::exit(1)
}
pub struct AppLauncher {
settings: AppSettings,
}
impl AppLauncher {
pub fn new() -> Self {
Self {
settings: AppSettings::default(),
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.settings.window_title = title.into();
self
}
pub fn with_size(mut self, width: u32, height: u32) -> Self {
self.settings.initial_width = width;
self.settings.initial_height = height;
self.settings.initial_size_explicit = true;
self
}
pub fn with_fonts(mut self, fonts: &'static [&'static [u8]]) -> Self {
self.settings.fonts = Some(fonts);
self
}
pub fn with_android_use_system_fonts(mut self, use_system_fonts: bool) -> Self {
self.settings.android_use_system_fonts = use_system_fonts;
self
}
pub fn with_android_overlay_window(mut self, options: AndroidOverlayWindowOptions) -> Self {
self.settings.android_overlay_window = Some(options);
self
}
pub fn with_headless(mut self, headless: bool) -> Self {
self.settings.headless = headless;
self
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub fn with_fps_counter(mut self, enabled: bool) -> Self {
self.settings.dev_options.fps_counter = enabled;
self
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub fn with_frame_pacing_mode(mut self, mode: FramePacingMode) -> Self {
self.settings.frame_pacing_mode = mode;
self.settings.dev_options.frame_pacing_mode = mode;
self.settings.frame_pacing_explicit = true;
self
}
#[cfg(not(all(feature = "desktop", feature = "renderer-wgpu")))]
pub fn with_frame_pacing_mode(self, mode: cranpose_app_shell::FramePacingMode) -> Self {
let _ = mode;
self
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub fn with_frame_pacing_controls(mut self, enabled: bool) -> Self {
self.settings.dev_options.frame_pacing_controls = enabled;
if enabled {
self.settings.dev_options.fps_counter = true;
}
self
}
#[cfg(not(all(feature = "desktop", feature = "renderer-wgpu")))]
pub fn with_frame_pacing_controls(self, enabled: bool) -> Self {
let _ = enabled;
self
}
#[cfg(not(all(feature = "desktop", feature = "renderer-wgpu")))]
pub fn with_fps_counter(self, enabled: bool) -> Self {
let _ = enabled;
self
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
pub fn with_recording(mut self, path: impl Into<PathBuf>) -> Self {
self.settings.record_to = Some(path.into());
self
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
pub fn with_test_driver(
mut self,
driver: impl FnOnce(crate::desktop::Robot) + Send + 'static,
) -> Self {
self.settings.test_driver = Some(Box::new(driver));
if !self.settings.frame_pacing_explicit {
self.settings.frame_pacing_mode = FramePacingMode::NoVsync;
self.settings.dev_options.frame_pacing_mode = FramePacingMode::NoVsync;
}
self
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
#[doc(hidden)]
pub fn with_robot_app_hook(
mut self,
hook: impl FnMut(String, String) -> Result<Option<String>, String> + 'static,
) -> Self {
self.settings.robot_app_hook = Some(Box::new(hook));
self
}
#[cfg(all(
feature = "desktop",
feature = "renderer-wgpu",
not(target_os = "android")
))]
pub fn try_run(self, content: impl FnMut() + 'static) -> Result<(), LaunchError> {
let mut content = content;
crate::desktop::try_run(self.settings, move || {
crate::ProvideUriHandler(|| {
content();
});
})
}
#[cfg(all(
feature = "desktop",
feature = "renderer-wgpu",
not(target_os = "android")
))]
pub fn run(self, content: impl FnMut() + 'static) -> ! {
self.try_run(content)
.unwrap_or_else(|error| exit_after_launch_error("desktop launch failed", error));
std::process::exit(0)
}
#[cfg(all(
feature = "desktop",
feature = "renderer-wgpu",
not(target_os = "android")
))]
pub fn try_run_windows(mut self, content: impl FnMut() + 'static) -> Result<(), LaunchError> {
self.settings.primary_window_visible = false;
self.try_run(content)
}
#[cfg(all(
feature = "desktop",
feature = "renderer-wgpu",
not(target_os = "android")
))]
pub fn run_windows(self, content: impl FnMut() + 'static) -> ! {
self.try_run_windows(content)
.unwrap_or_else(|error| exit_after_launch_error("desktop launch failed", error));
std::process::exit(0)
}
#[cfg(all(feature = "ios", feature = "renderer-wgpu", target_os = "ios"))]
pub fn try_run(self, content: impl FnMut() + 'static) -> Result<(), LaunchError> {
let mut content = content;
crate::ios::try_run(self.settings, move || {
crate::ProvideUriHandler(|| {
content();
});
})
}
#[cfg(all(feature = "ios", feature = "renderer-wgpu", target_os = "ios"))]
pub fn run(self, content: impl FnMut() + 'static) -> ! {
self.try_run(content)
.unwrap_or_else(|error| exit_after_launch_error("iOS launch failed", error));
std::process::exit(0)
}
#[cfg(all(feature = "android", target_os = "android"))]
pub fn run(self, app: android_activity::AndroidApp, content: impl FnMut() + 'static) {
let mut content = content;
crate::android::run(app, self.settings, move || {
crate::ProvideUriHandler(|| {
content();
});
});
}
#[cfg(all(feature = "web", feature = "renderer-wgpu", target_arch = "wasm32"))]
pub async fn run_web(
self,
canvas_id: &str,
content: impl FnMut() + 'static,
) -> Result<(), wasm_bindgen::JsValue> {
let mut content = content;
crate::web::run(canvas_id, self.settings, move || {
crate::ProvideUriHandler(|| {
content();
});
})
.await
}
}
impl Default for AppLauncher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn android_overlay_options_default_to_touch_only_top_left_window() {
let options = AndroidOverlayWindowOptions::new(320, 180);
assert_eq!(options.width, 320);
assert_eq!(options.height, 180);
assert_eq!(options.x, 0);
assert_eq!(options.y, 0);
assert!(!options.focusable);
assert!(options.is_valid());
}
#[test]
fn android_overlay_options_apply_position_and_focus() {
let options = AndroidOverlayWindowOptions::new(320, 180)
.with_position(12, 34)
.with_focusable(true);
assert_eq!(options.x, 12);
assert_eq!(options.y, 34);
assert!(options.focusable);
}
#[test]
fn android_overlay_options_reject_zero_size() {
assert!(!AndroidOverlayWindowOptions::new(0, 180).is_valid());
assert!(!AndroidOverlayWindowOptions::new(320, 0).is_valid());
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu"))]
#[test]
fn production_apps_default_to_vsync_frame_pacing() {
assert_eq!(
AppSettings::default().frame_pacing_mode,
FramePacingMode::Vsync
);
assert_eq!(FramePacingMode::default(), FramePacingMode::Vsync);
}
#[cfg(all(feature = "desktop", feature = "renderer-wgpu", feature = "robot"))]
#[test]
fn robot_test_driver_defaults_to_uncapped_frame_pacing() {
let launcher = AppLauncher::new().with_test_driver(|_| {});
assert_eq!(
launcher.settings.frame_pacing_mode,
FramePacingMode::NoVsync
);
let pinned = AppLauncher::new()
.with_frame_pacing_mode(FramePacingMode::Hard60)
.with_test_driver(|_| {});
assert_eq!(pinned.settings.frame_pacing_mode, FramePacingMode::Hard60);
let pinned_after = AppLauncher::new()
.with_test_driver(|_| {})
.with_frame_pacing_mode(FramePacingMode::Vsync);
assert_eq!(
pinned_after.settings.frame_pacing_mode,
FramePacingMode::Vsync
);
}
}