use crate::{
AppEnvironment, Logging, ModifyView, Puppeteer, PuppeteerError, PuppeteerResult, StaticAsset,
UiEvent, UiPaint,
};
use tao::{
dpi::PhysicalSize,
event::{Event, StartCause, WindowEvent},
event_loop::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopClosed, EventLoopProxy,
EventLoopWindowTarget,
},
monitor::MonitorHandle,
window::{Window, WindowBuilder},
};
use tracing::Level;
use wry::{http::Request, WebView, WebViewBuilder};
#[derive(Debug, Clone)]
pub struct ActiveAppEnv {
pub app_name: &'static str, pub env: AppEnvironment,
pub primary_monitor: Option<MonitorHandle>,
pub current_monitor: Option<MonitorHandle>,
pub available_monitors: Vec<MonitorHandle>,
pub fonts: &'static [StaticAsset],
}
pub struct PuppeteerApp<T: Puppeteer + 'static + Send + Sync> {
pub env: ActiveAppEnv,
event_loop: EventLoop<UiEvent<T>>,
proxy: EventLoopProxy<UiEvent<T>>,
}
impl<T> PuppeteerApp<T>
where
T: Puppeteer + 'static + Send + Sync,
{
pub fn init(app_name: &'static str) -> Self {
let event_loop = EventLoopBuilder::<UiEvent<T>>::with_user_event().build();
Logging::new(app_name).log("INITIALIZED EVENT_LOOP");
let proxy = event_loop.create_proxy();
Logging::new(app_name).log("INITIALIZED EVENT_LOOP PROXY");
PuppeteerApp {
event_loop,
proxy,
env: ActiveAppEnv {
app_name,
env: AppEnvironment::init(),
primary_monitor: Option::default(),
current_monitor: Option::default(),
available_monitors: Vec::default(),
fonts: &[StaticAsset {
name: "",
bytes: &[0u8],
}],
},
}
}
pub fn with_fonts(mut self, fonts: &'static [StaticAsset]) -> Self {
self.env.fonts = fonts;
T::shell().add_fonts(&self.env);
self
}
pub async fn start(mut self) -> PuppeteerResult<()> {
let (webview, window) =
PuppeteerApp::<T>::create_webview(&self.event_loop, self.proxy.clone(), &mut self.env)?;
self.event_loop
.run(move |event, _event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => {
let view_data = T::splashscreen();
Self::eval_script_exit_on_error(self.env.app_name, &webview, &view_data);
PuppeteerApp::<T>::send_event(
self.proxy.clone(),
self.env.clone(),
UiEvent::Init,
);
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*control_flow = ControlFlow::Exit;
}
Event::UserEvent(update_view) => match update_view {
UiEvent::Init => {
PuppeteerApp::<T>::send_init_event(
self.proxy.clone(),
self.env.clone(),
);
}
UiEvent::Close => {
Logging::new(self.env.app_name).log("REQUESTED TO CLOSE WINDOW");
*control_flow = ControlFlow::Exit;
std::process::exit(0)
}
UiEvent::Minimize => window.set_minimized(true),
UiEvent::Maximize => window.set_maximized(!window.is_maximized()),
UiEvent::Drag => {
if window.drag_window().is_err() {
PuppeteerApp::<T>::send_event(
self.proxy.clone(),
self.env.clone(),
UiEvent::Error(PuppeteerError::TaoEventLoopClosed),
);
}
}
UiEvent::Error(error) => {
let app_env = self.env.clone();
let local_proxy = self.proxy.clone();
smol::spawn(async move {
let outcome = T::error_handler(error).await;
PuppeteerApp::<T>::send_event(
local_proxy,
app_env,
UiEvent::Paint(outcome),
);
})
.detach();
}
UiEvent::Custom(custom_event) => {
PuppeteerApp::<T>::send_event_from_future(
self.proxy.clone(),
self.env.clone(),
custom_event,
);
}
UiEvent::Paint(paint_data) => match paint_data {
ModifyView::ComputeWithIdData { func, .. } => {
PuppeteerApp::<T>::callback_script_by_id(
self.env.app_name,
&webview,
self.proxy.clone(),
paint_data,
func,
)
}
ModifyView::ComputeInputWithIdData { func, .. } => {
PuppeteerApp::<T>::callback_script_by_id(
self.env.app_name,
&webview,
self.proxy.clone(),
paint_data,
func,
)
}
ModifyView::Skip => (),
_ => PuppeteerApp::<T>::eval_script_exit_on_error(
self.env.app_name,
&webview,
&paint_data,
),
},
},
_ => (),
}
});
}
fn send_init_event(proxy: EventLoopProxy<UiEvent<T>>, app_env: ActiveAppEnv) {
smol::spawn(async move {
let outcome = T::init(&app_env).await;
PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Paint(outcome)),
app_env.app_name,
);
})
.detach()
}
fn send_event_from_future(
proxy: EventLoopProxy<UiEvent<T>>,
app_env: ActiveAppEnv,
mut event: T,
) {
smol::spawn(async move {
let outcome = event.event_handler(&app_env).await;
PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Paint(outcome)),
app_env.app_name,
);
})
.detach()
}
fn send_event(proxy: EventLoopProxy<UiEvent<T>>, app_env: ActiveAppEnv, event: UiEvent<T>) {
smol::spawn(async move {
PuppeteerApp::<T>::proxy_error_handler(proxy.send_event(event), app_env.app_name);
})
.detach();
}
fn create_webview(
event_loop: &EventLoopWindowTarget<UiEvent<T>>,
proxy: EventLoopProxy<UiEvent<T>>,
app_env: &mut ActiveAppEnv,
) -> PuppeteerResult<(WebView, Window)> {
let window = WindowBuilder::new()
.with_title(app_env.app_name)
.with_decorations(false)
.build(event_loop)?;
Logging::new(app_env.app_name).log("INITIALIZED WINDOW");
let primary_monitor = event_loop.primary_monitor();
let current_monitor = window.current_monitor();
if let Some(monitor_found) = primary_monitor.as_ref() {
window.set_inner_size(PhysicalSize::new(
monitor_found.size().width as f32 * T::window_size(),
monitor_found.size().height as f32 * T::window_size(),
));
Logging::new(app_env.app_name).log(&format!("{:?}", monitor_found));
} else {
Logging::new(app_env.app_name).log("COULD NOT IDENTIFY PRIMARY MONITOR");
}
window.available_monitors().for_each(|monitor| {
Logging::new(app_env.app_name)
.log(format!("FOUND MONITOR - {:#?}", &monitor).as_str());
app_env.available_monitors.push(monitor);
});
app_env.primary_monitor = primary_monitor;
app_env.current_monitor = current_monitor;
let handler = PuppeteerApp::<T>::handler(proxy, app_env.clone());
let devtools_enabled = cfg!(debug_assertions);
let shell = T::shell();
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
))]
let webview_builder = WebViewBuilder::new(&window);
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "ios",
target_os = "android"
)))]
let webview_builder = {
use tao::platform::unix::WindowExtUnix;
use wry::WebViewBuilderExtUnix;
let vbox = match window.default_vbox() {
Some(vbox) => vbox,
None => return Err(PuppeteerError::GtkError),
};
WebViewBuilder::new_gtk(vbox)
};
let current_html = shell.to_html();
#[cfg(target_os = "windows")]
if current_html.as_ref().len() * 2 > 2 * 1024 * 1024 {
return Err(PuppeteerError::StringTooLarge);
}
let webview = webview_builder
.with_html(current_html)
.with_devtools(devtools_enabled)
.with_ipc_handler(handler)
.build()?;
Ok((webview, window))
}
fn handler(
proxy: EventLoopProxy<UiEvent<T>>,
app_env: ActiveAppEnv,
) -> Box<dyn Fn(Request<String>) + 'static> {
let outcome = move |req: Request<String>| match req.body().as_str() {
"minimize" => PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Minimize),
app_env.app_name,
),
"maximize" => PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Maximize),
app_env.app_name,
),
"drag_window" => PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Drag),
app_env.app_name,
),
"close_window" => PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Close),
app_env.app_name,
),
_ => {
let req_parse = T::parse(&req.body());
PuppeteerApp::<T>::proxy_error_handler(
proxy.send_event(UiEvent::Custom(req_parse)),
app_env.app_name,
)
}
};
Box::new(outcome)
}
fn proxy_error_handler(
value: Result<(), EventLoopClosed<UiEvent<T>>>,
log_filter_name: &'static str,
) {
match value {
Ok(_) => (),
Err(error) => {
Logging::new(log_filter_name)
.with_level(Level::ERROR)
.log(error.to_string().as_str());
std::process::exit(1);
}
}
}
fn eval_script_exit_on_error(app_name: &'static str, webview: &WebView, content: &ModifyView) {
match webview.evaluate_script(&content.to_html()) {
Ok(_) => (),
Err(error) => {
Logging::new(app_name)
.with_level(Level::ERROR)
.log(error.to_string().as_str());
std::process::exit(1);
}
}
}
fn callback_script_by_id(
app_name: &'static str,
webview: &WebView,
proxy: EventLoopProxy<UiEvent<T>>,
script: impl UiPaint,
callback_fn: crate::JsCallback,
) {
let callback = move |value: String| {
if proxy
.send_event(UiEvent::Paint(callback_fn(&value)))
.is_err()
{
Logging::new(app_name)
.with_level(Level::ERROR)
.log(PuppeteerError::TaoEventLoopClosed.to_string().as_str());
}
};
match webview.evaluate_script_with_callback(&script.to_html(), callback) {
Ok(_) => (),
Err(error) => {
Logging::new(app_name)
.with_level(Level::ERROR)
.log(error.to_string().as_str());
std::process::exit(1);
}
}
}
}