use std::collections::{HashMap, HashSet};
use std::sync::{Arc, LazyLock, Mutex};
use lingxia_platform::traits::app_runtime::AppRuntime;
use lingxia_platform::traits::ui::SurfaceContent;
pub use lingxia_platform::{Platform, PlatformError, set_windows_app_exit_handler};
use lingxia_webview::{NavigationPolicy, WebTag, WebViewController, WebViewDataMode};
static WINDOWS_APP_VISIBLE_WEBTAGS: LazyLock<Mutex<HashMap<String, HashSet<String>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn init(platform: Platform) -> crate::Result<crate::RuntimeInfo> {
crate::logging::init();
lingxia_webview::platform::windows::set_webview_user_data_dir(
platform.app_cache_dir().join("webview2"),
);
install_lifecycle_bridge();
install_url_surface_bridge();
install_aside_panel_bridge();
crate::init_with_platform(platform)
}
pub fn open_home_app(appid: &str) -> Result<(), String> {
lxapp::open_lxapp(appid, lxapp::LxAppStartupOptions::new(""))
.map(|_| ())
.map_err(|err| err.to_string())
}
pub fn set_default_window_size(width: i32, height: i32) {
lingxia_windows_contract::set_default_window_size(width, height);
}
pub fn resize_app_window_content(appid: &str, width: i32, height: i32) -> Result<(), String> {
let webview = current_page_webview(appid)?;
lingxia_windows_contract::resize_host_window_content(&webview.webtag(), width, height)
.map_err(|err| err.to_string())
}
pub fn set_surface_width(appid: &str, width: f64) -> bool {
lxapp::try_get(appid)
.map(|app| app.set_surface_width(width))
.unwrap_or(false)
}
pub fn set_surface_layout_metrics(appid: &str, width: f64, sidebar_width: f64) -> bool {
lxapp::try_get(appid)
.map(|app| app.set_surface_layout_metrics(width, sidebar_width))
.unwrap_or(false)
}
pub fn set_surface_sidebar_width(appid: &str, width: f64) -> bool {
lxapp::try_get(appid)
.map(|app| app.set_surface_sidebar_width(width))
.unwrap_or(false)
}
pub fn set_surface_viewport(appid: &str, width: f64, height: f64) -> bool {
lxapp::try_get(appid)
.map(|app| app.set_surface_viewport(width, height))
.unwrap_or(false)
}
fn current_page_webview(appid: &str) -> Result<std::sync::Arc<lingxia_webview::WebView>, String> {
let app = lxapp::try_get(appid).ok_or_else(|| format!("lxapp is not active: {appid}"))?;
let page = app.current_page().map_err(|err| err.to_string())?;
page.webview()
.ok_or_else(|| "page WebView is not ready".to_string())
}
fn install_lifecycle_bridge() {
lingxia_windows_contract::set_webview_visibility_handler(Arc::new(|webtag, visible| {
on_webview_visibility_changed(webtag, visible);
}));
}
fn install_aside_panel_bridge() {
lingxia_platform::install_windows_aside_panel_bridge();
}
fn install_url_surface_bridge() {
lingxia_platform::set_windows_url_surface_handler(Arc::new(|request| {
if request.content != SurfaceContent::Url {
return None;
}
let data_mode = if request.ephemeral_web_data {
WebViewDataMode::Ephemeral
} else {
WebViewDataMode::ProfileDefault
};
#[cfg(feature = "browser-runtime")]
if let Some(resolved) = resolve_url_surface_as_browser_tab(request, data_mode) {
return Some(resolved);
}
let webtag = WebTag::new(&request.app_id, &request.path, Some(request.session_id));
let url = request.path.clone();
let url_callback = request.url_callback;
let session = lingxia_webview::WebViewBuilder::browser(webtag)
.data_mode(data_mode)
.on_navigation(move |request| url_surface_navigation_policy(url_callback, &request.url))
.create();
std::mem::drop(crate::task::spawn(async move {
match session.wait_ready().await {
Ok(webview) => {
if let Err(err) = webview.load_url(&url) {
log::error!("URL surface failed to load {url}: {err}");
}
}
Err(err) => log::error!("URL surface webview create failed for {url}: {err}"),
}
}));
Some(lingxia_platform::WindowsUrlSurfaceWebTag {
app_id: request.app_id.clone(),
path: request.path.clone(),
session_id: request.session_id,
cleanup: None,
})
}));
}
fn url_uses_scheme(url: &str, expected: &str) -> bool {
url.trim()
.split_once(':')
.is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case(expected))
}
fn url_surface_navigation_policy(url_callback: bool, url: &str) -> NavigationPolicy {
if url_callback && url_uses_scheme(url, "file") {
NavigationPolicy::Cancel
} else {
NavigationPolicy::Allow
}
}
#[cfg(feature = "browser-runtime")]
fn resolve_url_surface_as_browser_tab(
request: &lingxia_platform::traits::ui::SurfaceRequest,
data_mode: WebViewDataMode,
) -> Option<lingxia_platform::WindowsUrlSurfaceWebTag> {
let tab_id = crate::browser::open_standalone_for_app(
&request.app_id,
request.session_id,
&request.path,
None,
data_mode,
request.url_callback,
)
.inspect_err(|err| log::warn!("URL surface browser tab failed for {}: {err}", request.path))
.ok()?;
let tab = crate::browser::tab_summary(&tab_id)?;
let close_tab_id = tab_id.clone();
Some(lingxia_platform::WindowsUrlSurfaceWebTag {
app_id: crate::browser::APP_ID.to_string(),
path: tab.path,
session_id: tab.session_id,
cleanup: Some(Arc::new(move || {
let _ = crate::browser::close(&close_tab_id);
})),
})
}
fn on_webview_visibility_changed(webtag: &WebTag, visible: bool) {
let (appid, path) = webtag.extract_parts();
if appid.is_empty() || path.is_empty() {
return;
}
if let Err(err) = lxapp::notify_page_host_visibility(&appid, &path, visible) {
log::debug!(
"Windows page visibility event ignored for {} visible={}: {}",
webtag,
visible,
err
);
}
let app_event = update_app_visible_webtags(&appid, webtag.key(), visible);
if let Some(visible) = app_event
&& let Err(err) = lxapp::notify_lxapp_host_visibility(&appid, visible)
{
log::debug!(
"Windows app visibility event ignored for {} visible={}: {}",
appid,
visible,
err
);
}
}
fn update_app_visible_webtags(appid: &str, webtag_key: &str, visible: bool) -> Option<bool> {
let Ok(mut visible_webtags) = WINDOWS_APP_VISIBLE_WEBTAGS.lock() else {
return None;
};
let webtags = visible_webtags.entry(appid.to_string()).or_default();
let was_visible = !webtags.is_empty();
if visible {
webtags.insert(webtag_key.to_string());
} else {
webtags.remove(webtag_key);
}
let is_visible = !webtags.is_empty();
if !is_visible {
visible_webtags.remove(appid);
}
(was_visible != is_visible).then_some(is_visible)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn app_visibility_is_aggregated_across_webtags() {
WINDOWS_APP_VISIBLE_WEBTAGS.lock().unwrap().clear();
assert_eq!(
update_app_visible_webtags("app", "app:main", true),
Some(true)
);
assert_eq!(update_app_visible_webtags("app", "app:panel", true), None);
assert_eq!(update_app_visible_webtags("app", "app:panel", false), None);
assert_eq!(
update_app_visible_webtags("app", "app:main", false),
Some(false)
);
}
#[test]
fn standard_url_surface_policy_keeps_file_for_browsers_only() {
assert_eq!(
url_surface_navigation_policy(false, "file:///C:/Temp/report.html"),
NavigationPolicy::Allow
);
assert_eq!(
url_surface_navigation_policy(true, "FILE:/C:/Temp/report.html"),
NavigationPolicy::Cancel
);
assert_eq!(
url_surface_navigation_policy(true, "http://127.0.0.1:18080/authorize"),
NavigationPolicy::Allow
);
}
}