use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use lingxia_surface::{Edge, LayoutPresentationPlan, SurfaceInteraction};
use lingxia_webview::WebTag;
use lingxia_webview::platform::windows::{WindowsWebViewHandler, find_webview_handler};
use lingxia_webview::runtime as webview_runtime;
use lingxia_windows_contract::{
ASIDE_LXAPP_PANEL_ID, WindowsAsidePanelEvent, WindowsAsidePanelTab, WindowsNavAnimation,
WindowsPanelPosition, active_host_window_is_device_framed,
configure_webview_surface_interaction, hide_host_panel, hide_webview_window,
navigate_webview_window, present_webview_as_overlay, present_webview_in_active_group,
refresh_aside_panel, set_aside_panel_tabs, set_webview_close_handler,
set_windows_aside_panel_event_handler, show_webview_as_adaptive_panel, show_webview_as_panel,
show_webview_window, show_webview_window_with_content_size,
};
use super::request_windows_app_exit;
use crate::error::PlatformError;
use crate::traits::app_runtime::{AnimationType, LxAppOpenMode};
use crate::traits::ui::{SurfaceContent, SurfaceKind, SurfaceRequest, SurfaceRole};
static WINDOWS_SHOW_SEQUENCE: AtomicU64 = AtomicU64::new(1);
static WINDOWS_SHOW_REQUESTS: LazyLock<Mutex<HashMap<String, u64>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[derive(Clone)]
struct RuntimeLxappAside {
webtag: WebTag,
title: String,
}
static RUNTIME_LXAPP_ASIDES: LazyLock<Mutex<HashMap<String, RuntimeLxappAside>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static LAST_LAYOUT_PLAN: Mutex<Option<LayoutPresentationPlan>> = Mutex::new(None);
pub type LayoutPlanHandler = Arc<dyn Fn(&LayoutPresentationPlan) + Send + Sync>;
static LAYOUT_PLAN_HANDLER: Mutex<Option<LayoutPlanHandler>> = Mutex::new(None);
pub type ManagedAsideEventHandler = Arc<dyn Fn(WindowsAsidePanelEvent) + Send + Sync>;
static MANAGED_ASIDE_EVENT_HANDLER: Mutex<Option<ManagedAsideEventHandler>> = Mutex::new(None);
pub fn set_windows_layout_plan_handler(handler: LayoutPlanHandler) {
if let Ok(mut slot) = LAYOUT_PLAN_HANDLER.lock() {
*slot = Some(handler);
}
}
pub fn set_windows_managed_aside_event_handler(handler: ManagedAsideEventHandler) {
if let Ok(mut slot) = MANAGED_ASIDE_EVENT_HANDLER.lock() {
*slot = Some(handler);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WindowsCloseAction {
ExitApp,
HideWindow,
}
pub(super) fn show_webtag_window(
webtag: WebTag,
title: String,
activate: bool,
open_mode: LxAppOpenMode,
panel_id: String,
) {
if open_mode == LxAppOpenMode::Panel {
remember_runtime_lxapp_aside(&panel_id, webtag.clone(), title.clone());
}
let request_key = show_request_key(&webtag, open_mode, &panel_id);
let request_id = remember_show_request(&request_key);
if let Some(handler) = find_webview_handler(&webtag) {
if show_request_is_current(&request_key, request_id) {
install_close_handler(&webtag, close_action_for_mode(open_mode));
show_webview_handler_for_mode(handler, &title, activate, open_mode, &panel_id);
}
return;
}
let _ = thread::Builder::new()
.name(format!("lingxia-windows-show-{}", webtag.key()))
.spawn(move || {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if !show_request_is_current(&request_key, request_id) {
return;
}
if let Some(handler) = find_webview_handler(&webtag) {
install_close_handler(&webtag, close_action_for_mode(open_mode));
show_webview_handler_for_mode(handler, &title, activate, open_mode, &panel_id);
return;
}
thread::sleep(Duration::from_millis(50));
}
log::error!("Timed out waiting for Windows WebView {}", webtag.key());
});
}
pub(super) fn navigate_webtag_window(webtag: WebTag, title: String, animation: AnimationType) {
let animation = nav_animation(animation);
let request_key = show_request_key(&webtag, LxAppOpenMode::Normal, "");
let request_id = remember_show_request(&request_key);
if let Some(handler) = find_webview_handler(&webtag) {
if show_request_is_current(&request_key, request_id) {
show_webview_handler_navigate(handler, &title, animation);
}
return;
}
let _ = thread::Builder::new()
.name(format!("lingxia-windows-navigate-{}", webtag.key()))
.spawn(move || {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if !show_request_is_current(&request_key, request_id) {
return;
}
if let Some(handler) = find_webview_handler(&webtag) {
show_webview_handler_navigate(handler, &title, animation);
return;
}
thread::sleep(Duration::from_millis(50));
}
log::error!(
"Timed out waiting for Windows navigation WebView {}",
webtag.key()
);
});
}
fn nav_animation(animation: AnimationType) -> WindowsNavAnimation {
match animation {
AnimationType::Forward => WindowsNavAnimation::Forward,
AnimationType::Backward => WindowsNavAnimation::Backward,
AnimationType::None => WindowsNavAnimation::None,
}
}
pub(super) fn hide_lxapp_window(appid: &str, session_id: u64) {
invalidate_show_request(&format!("main:{appid}#{session_id}"));
let removed_panel_ids = RUNTIME_LXAPP_ASIDES
.lock()
.ok()
.map(|mut entries| {
let ids = entries
.iter()
.filter(|(_, entry)| {
entry.webtag.extract_appid() == appid
&& entry.webtag.session_id() == Some(session_id)
})
.map(|(id, _)| id.clone())
.collect::<Vec<_>>();
for id in &ids {
entries.remove(id);
}
ids
})
.unwrap_or_default();
for panel_id in removed_panel_ids {
invalidate_show_request(&format!("panel:{panel_id}"));
}
for webtag in webview_runtime::list_webviews() {
if webtag.extract_appid() == appid && webtag.session_id() == Some(session_id) {
let _ = hide_webview_window(&webtag);
}
}
}
fn show_webview_handler_for_mode(
handler: WindowsWebViewHandler,
title: &str,
activate: bool,
open_mode: LxAppOpenMode,
panel_id: &str,
) {
let result = match open_mode {
LxAppOpenMode::Panel => {
if sync_runtime_lxapp_asides_from_last_plan(panel_id) {
Ok(())
} else if LAST_LAYOUT_PLAN
.lock()
.map(|plan| plan.is_some())
.unwrap_or(false)
{
Ok(())
} else {
show_webview_as_panel(&handler.webtag(), title, panel_id)
}
}
LxAppOpenMode::Normal if active_host_window_is_device_framed() => {
present_webview_in_active_group(&handler.webtag())
}
LxAppOpenMode::Normal => show_webview_window(&handler.webtag(), title, activate),
};
if let Err(err) = result {
log::warn!(
"Failed to show Windows WebView window {}: {}",
handler.webtag().key(),
err
);
}
}
fn remember_runtime_lxapp_aside(panel_id: &str, webtag: WebTag, title: String) {
if panel_id.trim().is_empty() {
return;
}
if let Ok(mut entries) = RUNTIME_LXAPP_ASIDES.lock() {
entries.insert(panel_id.to_string(), RuntimeLxappAside { webtag, title });
}
}
fn sync_runtime_lxapp_asides_from_last_plan(panel_id: &str) -> bool {
let plan = LAST_LAYOUT_PLAN.lock().ok().and_then(|plan| plan.clone());
plan.as_ref().is_some_and(|plan| {
plan.aside_slots.iter().any(|slot| {
slot.kind == lingxia_surface::SlotKind::Lxapp
&& slot.children.iter().any(|child| child == panel_id)
}) && sync_runtime_lxapp_asides(plan)
})
}
fn show_webview_handler_navigate(
handler: WindowsWebViewHandler,
title: &str,
animation: WindowsNavAnimation,
) {
let webtag = handler.webtag();
if let Err(err) = navigate_webview_window(&webtag, title, false, animation) {
log::warn!(
"Failed to navigate Windows WebView window {}: {}",
webtag.key(),
err
);
}
}
fn show_request_key(webtag: &WebTag, open_mode: LxAppOpenMode, panel_id: &str) -> String {
match open_mode {
LxAppOpenMode::Normal => {
format!(
"main:{}#{}",
webtag.extract_appid(),
webtag
.session_id()
.map(|session| session.to_string())
.unwrap_or_else(|| "0".to_string())
)
}
LxAppOpenMode::Panel => format!("panel:{panel_id}"),
}
}
fn remember_show_request(key: &str) -> u64 {
let request_id = WINDOWS_SHOW_SEQUENCE.fetch_add(1, Ordering::Relaxed);
if let Ok(mut requests) = WINDOWS_SHOW_REQUESTS.lock() {
requests.insert(key.to_string(), request_id);
}
request_id
}
fn show_request_is_current(key: &str, request_id: u64) -> bool {
WINDOWS_SHOW_REQUESTS
.lock()
.ok()
.and_then(|requests| requests.get(key).copied())
== Some(request_id)
}
fn invalidate_show_request(key: &str) {
if let Ok(mut requests) = WINDOWS_SHOW_REQUESTS.lock() {
requests.remove(key);
}
}
fn close_action_for_mode(open_mode: LxAppOpenMode) -> WindowsCloseAction {
match open_mode {
LxAppOpenMode::Normal => WindowsCloseAction::ExitApp,
LxAppOpenMode::Panel => WindowsCloseAction::HideWindow,
}
}
fn install_close_handler(webtag: &WebTag, action: WindowsCloseAction) {
let webtag_for_close = webtag.clone();
set_webview_close_handler(
webtag,
Arc::new(move || match action {
WindowsCloseAction::ExitApp => request_windows_app_exit(),
WindowsCloseAction::HideWindow => {
let _ = hide_webview_window(&webtag_for_close);
}
}),
);
}
type SurfaceClosedHandler = Arc<dyn Fn(&str, &str) + Send + Sync>;
static SURFACE_CLOSED_HANDLER: Mutex<Option<SurfaceClosedHandler>> = Mutex::new(None);
pub fn set_windows_surface_closed_handler(handler: SurfaceClosedHandler) {
if let Ok(mut slot) = SURFACE_CLOSED_HANDLER.lock() {
*slot = Some(handler);
}
}
fn notify_surface_closed(id: &str, reason: &str) {
let handler = SURFACE_CLOSED_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some(handler) = handler {
handler(id, reason);
}
}
type PageVisibilityHandler = Arc<dyn Fn(&str, bool) -> bool + Send + Sync>;
static PAGE_VISIBILITY_HANDLER: Mutex<Option<PageVisibilityHandler>> = Mutex::new(None);
pub fn set_windows_page_visibility_handler(handler: PageVisibilityHandler) {
if let Ok(mut slot) = PAGE_VISIBILITY_HANDLER.lock() {
*slot = Some(handler);
}
}
fn notify_page_visibility(page_instance_id: &str, visible: bool) -> bool {
let handler = PAGE_VISIBILITY_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some(handler) = handler {
return handler(page_instance_id, visible);
}
false
}
fn notify_page_visibility_when_ready(page_instance_id: String, visible: bool) {
if notify_page_visibility(&page_instance_id, visible) {
return;
}
let _ = thread::Builder::new()
.name(format!("lingxia-surface-visible-{page_instance_id}"))
.spawn(move || {
for _ in 0..100 {
thread::sleep(Duration::from_millis(50));
if notify_page_visibility(&page_instance_id, visible) {
return;
}
}
});
}
type SurfaceDisposeHandler = Arc<dyn Fn(&str, &str) + Send + Sync>;
static SURFACE_DISPOSE_HANDLER: Mutex<Option<SurfaceDisposeHandler>> = Mutex::new(None);
pub fn set_windows_surface_dispose_handler(handler: SurfaceDisposeHandler) {
if let Ok(mut slot) = SURFACE_DISPOSE_HANDLER.lock() {
*slot = Some(handler);
}
}
fn dispose_surface_page(page_instance_id: &str, reason: &str) {
let handler = SURFACE_DISPOSE_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some(handler) = handler {
handler(page_instance_id, reason);
}
}
type ManagedSurfaceVisibleHandler = Arc<dyn Fn(&str, bool, &str) -> bool + Send + Sync>;
static MANAGED_SURFACE_VISIBLE_HANDLER: Mutex<Option<ManagedSurfaceVisibleHandler>> =
Mutex::new(None);
pub fn set_windows_managed_surface_visible_handler(handler: ManagedSurfaceVisibleHandler) {
if let Ok(mut slot) = MANAGED_SURFACE_VISIBLE_HANDLER.lock() {
*slot = Some(handler);
}
}
pub(super) fn set_managed_surface_visible(
id: &str,
visible: bool,
edge: Option<&str>,
) -> Result<(), PlatformError> {
let handler = MANAGED_SURFACE_VISIBLE_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
.ok_or_else(|| {
PlatformError::NotSupported(
"managed surfaces are not supported on this Windows host".to_string(),
)
})?;
if handler(id, visible, edge.unwrap_or_default()) {
Ok(())
} else {
Err(PlatformError::InvalidParameter(format!(
"unknown managed surface: {id}"
)))
}
}
type ManagedSurfaceToggleHandler = Arc<dyn Fn(&str) -> bool + Send + Sync>;
static MANAGED_SURFACE_TOGGLE_HANDLER: Mutex<Option<ManagedSurfaceToggleHandler>> =
Mutex::new(None);
pub fn set_windows_managed_surface_toggle_handler(handler: ManagedSurfaceToggleHandler) {
if let Ok(mut slot) = MANAGED_SURFACE_TOGGLE_HANDLER.lock() {
*slot = Some(handler);
}
}
pub(super) fn toggle_managed_surface(id: &str) -> Result<(), PlatformError> {
let handler = MANAGED_SURFACE_TOGGLE_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
.ok_or_else(|| {
PlatformError::NotSupported(
"managed surfaces are not supported on this Windows host".to_string(),
)
})?;
if handler(id) {
Ok(())
} else {
Err(PlatformError::InvalidParameter(format!(
"unknown managed surface: {id}"
)))
}
}
#[derive(Clone)]
pub struct WindowsUrlSurfaceWebTag {
pub app_id: String,
pub path: String,
pub session_id: u64,
pub cleanup: Option<Arc<dyn Fn() + Send + Sync>>,
}
type UrlSurfaceHandler =
Arc<dyn Fn(&SurfaceRequest) -> Option<WindowsUrlSurfaceWebTag> + Send + Sync>;
static URL_SURFACE_HANDLER: Mutex<Option<UrlSurfaceHandler>> = Mutex::new(None);
pub fn set_windows_url_surface_handler(handler: UrlSurfaceHandler) {
if let Ok(mut slot) = URL_SURFACE_HANDLER.lock() {
*slot = Some(handler);
}
}
fn resolve_url_surface(request: &SurfaceRequest) -> Option<WindowsUrlSurfaceWebTag> {
let handler = URL_SURFACE_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())?;
handler(request)
}
fn teardown_surface(entry: &SurfaceEntry, id: &str, reason: &str) {
notify_surface_closed(id, reason);
match &entry.page_instance_id {
Some(page_instance_id) => dispose_surface_page(page_instance_id, reason),
None => {
if let Some(cleanup) = &entry.cleanup {
cleanup();
}
webview_runtime::destroy_webview(&entry.webtag);
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct OverlayPlacement {
width: f64,
height: f64,
width_ratio: f64,
height_ratio: f64,
position: u8,
}
#[derive(Clone)]
struct SurfaceEntry {
webtag: WebTag,
kind: SurfaceKind,
role: SurfaceRole,
title: String,
page_instance_id: Option<String>,
cleanup: Option<Arc<dyn Fn() + Send + Sync>>,
placement: OverlayPlacement,
is_web: bool,
interaction: SurfaceInteraction,
}
#[derive(Clone, Copy)]
enum PresentationTarget {
Stored,
Overlay,
Aside {
edge: Option<Edge>,
preferred_size: Option<f64>,
grouped: bool,
},
}
static SURFACES: LazyLock<Mutex<HashMap<String, SurfaceEntry>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn surface_entry(id: &str) -> Option<SurfaceEntry> {
SURFACES.lock().ok().and_then(|map| map.get(id).cloned())
}
fn finite_or_zero(value: f64) -> f64 {
if value.is_finite() && value > 0.0 {
value
} else {
0.0
}
}
fn present_entry(id: &str, entry: &SurfaceEntry, target: PresentationTarget) -> Result<(), String> {
let result = match (entry.role, target) {
(SurfaceRole::Aside, PresentationTarget::Overlay) => {
present_webview_as_overlay(&entry.webtag, 0.0, 0.0, 1.0, 1.0, entry.placement.position)
}
(
SurfaceRole::Aside,
PresentationTarget::Aside {
edge,
preferred_size,
grouped,
},
) => show_webview_as_adaptive_panel(
&entry.webtag,
&entry.title,
if grouped { ASIDE_BROWSER_PANEL_ID } else { id },
panel_position_for(edge, entry.placement.position),
preferred_panel_size(preferred_size),
),
(SurfaceRole::Aside, PresentationTarget::Stored) => show_webview_as_adaptive_panel(
&entry.webtag,
&entry.title,
id,
panel_position_for(None, entry.placement.position),
None,
),
(SurfaceRole::Float, _) => {
let p = &entry.placement;
present_webview_as_overlay(
&entry.webtag,
p.width,
p.height,
p.width_ratio,
p.height_ratio,
p.position,
)
}
(SurfaceRole::Main, _) => match entry.kind {
SurfaceKind::Overlay => present_webview_in_active_group(&entry.webtag),
SurfaceKind::Window => show_webview_window_with_content_size(
&entry.webtag,
&entry.title,
true,
window_dimension(entry.placement.width),
window_dimension(entry.placement.height),
),
},
};
result.map_err(|err| err.to_string())?;
configure_webview_surface_interaction(
&entry.webtag,
entry.interaction.close_button,
entry.interaction.dismiss == lingxia_surface::FloatDismiss::TapOutside,
entry.interaction.modal,
)
.map_err(|err| err.to_string())
}
fn window_dimension(value: f64) -> Option<i32> {
(value.is_finite() && value > 0.0).then(|| value.round().clamp(1.0, i32::MAX as f64) as i32)
}
fn panel_position_for(edge: Option<Edge>, fallback_position: u8) -> WindowsPanelPosition {
match edge {
Some(Edge::Left) => WindowsPanelPosition::Left,
Some(Edge::Right) => WindowsPanelPosition::Right,
Some(Edge::Top) => WindowsPanelPosition::Top,
Some(Edge::Bottom) => WindowsPanelPosition::Bottom,
None => match fallback_position {
2 => WindowsPanelPosition::Left,
4 => WindowsPanelPosition::Top,
1 => WindowsPanelPosition::Bottom,
_ => WindowsPanelPosition::Right,
},
}
}
fn preferred_panel_size(value: Option<f64>) -> Option<i32> {
value
.filter(|value| value.is_finite() && *value > 0.0)
.map(|value| value.round().clamp(1.0, i32::MAX as f64) as i32)
}
fn hide_entry(entry: &SurfaceEntry) {
let _ = hide_webview_window(&entry.webtag);
if let Some(page_instance_id) = &entry.page_instance_id {
notify_page_visibility_when_ready(page_instance_id.clone(), false);
};
}
fn sync_runtime_lxapp_asides(plan: &LayoutPresentationPlan) -> bool {
let Some(slot) = plan
.aside_slots
.iter()
.find(|slot| slot.kind == lingxia_surface::SlotKind::Lxapp)
else {
set_aside_panel_tabs(ASIDE_LXAPP_PANEL_ID, Vec::new());
hide_all_runtime_lxapp_asides();
return false;
};
if slot.children.is_empty() {
set_aside_panel_tabs(ASIDE_LXAPP_PANEL_ID, Vec::new());
hide_all_runtime_lxapp_asides();
return false;
}
let entries = RUNTIME_LXAPP_ASIDES
.lock()
.ok()
.map(|entries| entries.clone())
.unwrap_or_default();
let active_id = slot
.active_child
.as_deref()
.filter(|id| slot.children.iter().any(|child| child == *id))
.or_else(|| slot.children.last().map(String::as_str));
let tabs = slot
.children
.iter()
.map(|id| WindowsAsidePanelTab {
surface_id: id.clone(),
title: entries
.get(id)
.map(|entry| entry.title.clone())
.filter(|title| !title.trim().is_empty())
.unwrap_or_else(|| id.clone()),
active: Some(id.as_str()) == active_id,
})
.collect();
set_aside_panel_tabs(ASIDE_LXAPP_PANEL_ID, tabs);
let aside_by_id: HashMap<_, _> = plan
.asides
.iter()
.map(|aside| (aside.id.as_str(), aside))
.collect();
if slot.visible
&& let Some(active_id) = active_id
&& let Some(entry) = entries.get(active_id)
&& find_webview_handler(&entry.webtag).is_some()
{
let aside = aside_by_id.get(active_id).copied();
let edge = slot.edge.or_else(|| aside.and_then(|aside| aside.edge));
let preferred_size = aside.and_then(|aside| aside.preferred_size);
let result = if slot.overlay {
let _ = hide_host_panel(ASIDE_LXAPP_PANEL_ID);
present_webview_in_active_group(&entry.webtag)
} else {
show_webview_as_adaptive_panel(
&entry.webtag,
&entry.title,
ASIDE_LXAPP_PANEL_ID,
panel_position_for(edge, 3),
preferred_panel_size(preferred_size),
)
};
if let Err(err) = result {
log::warn!(
"Failed to present lxapp aside {}: {}",
entry.webtag.key(),
err
);
}
}
for (id, entry) in entries {
if !slot.visible || Some(id.as_str()) != active_id {
let _ = hide_webview_window(&entry.webtag);
}
}
refresh_aside_panel(ASIDE_LXAPP_PANEL_ID);
true
}
fn hide_all_runtime_lxapp_asides() {
let _ = hide_host_panel(ASIDE_LXAPP_PANEL_ID);
let entries = RUNTIME_LXAPP_ASIDES
.lock()
.ok()
.map(|entries| entries.values().cloned().collect::<Vec<_>>())
.unwrap_or_default();
for entry in entries {
let _ = hide_webview_window(&entry.webtag);
}
}
use lingxia_windows_contract::ASIDE_BROWSER_PANEL_ID;
#[derive(Default)]
struct AsideBrowserGroup {
order: Vec<String>,
active: Option<String>,
placement: HashMap<String, (Option<Edge>, Option<f64>)>,
overlay: bool,
}
static ASIDE_BROWSER_GROUP: LazyLock<Mutex<AsideBrowserGroup>> =
LazyLock::new(|| Mutex::new(AsideBrowserGroup::default()));
fn aside_browser_group() -> std::sync::MutexGuard<'static, AsideBrowserGroup> {
ASIDE_BROWSER_GROUP
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn store_aside_browser_plan(
tabs: Vec<(String, Option<Edge>, Option<f64>)>,
active: Option<&str>,
overlay: bool,
) {
let mut group = aside_browser_group();
group.placement = tabs
.iter()
.map(|(id, edge, size)| (id.clone(), (*edge, *size)))
.collect();
group.order = tabs.into_iter().map(|(id, _, _)| id).collect();
group.active = active
.filter(|id| group.order.iter().any(|child| child == *id))
.map(str::to_string)
.or_else(|| group.order.last().cloned());
group.overlay = overlay;
}
fn sync_aside_browser_group() {
let (order, active, placement, overlay) = {
let group = aside_browser_group();
(
group.order.clone(),
group.active.clone(),
group.placement.clone(),
group.overlay,
)
};
let tabs: Vec<(String, SurfaceEntry)> = order
.iter()
.filter_map(|id| surface_entry(id).map(|entry| (id.clone(), entry)))
.collect();
let strip: Vec<WindowsAsidePanelTab> = tabs
.iter()
.map(|(id, entry)| WindowsAsidePanelTab {
surface_id: id.clone(),
title: aside_tab_title(&entry.title),
active: Some(id) == active.as_ref(),
})
.collect();
set_aside_panel_tabs(ASIDE_BROWSER_PANEL_ID, strip);
if active.is_none() {
let _ = hide_host_panel(ASIDE_BROWSER_PANEL_ID);
}
if let Some((id, entry)) = tabs.iter().find(|(id, _)| Some(id) == active.as_ref()) {
let (edge, preferred_size) = placement.get(id).copied().unwrap_or((None, None));
let target = if overlay {
let _ = hide_host_panel(ASIDE_BROWSER_PANEL_ID);
PresentationTarget::Overlay
} else {
PresentationTarget::Aside {
edge,
preferred_size,
grouped: true,
}
};
present_surface_when_ready(entry.webtag.clone(), id.clone(), target);
}
for (id, entry) in &tabs {
if Some(id) != active.as_ref() {
let _ = hide_webview_window(&entry.webtag);
}
}
refresh_aside_panel(ASIDE_BROWSER_PANEL_ID);
}
fn aside_tab_title(url: &str) -> String {
let host = url
.split_once("://")
.map(|(_, rest)| rest.split(['/', '?', '#']).next().unwrap_or(rest))
.map(|authority| authority.rsplit('@').next().unwrap_or(authority))
.unwrap_or("")
.trim();
if host.is_empty() {
url.to_string()
} else {
host.to_string()
}
}
pub fn install_windows_aside_panel_bridge() {
set_windows_aside_panel_event_handler(Arc::new(handle_aside_panel_event));
}
fn handle_aside_panel_event(event: WindowsAsidePanelEvent) {
let panel_id = match &event {
WindowsAsidePanelEvent::TabClick { panel_id, .. }
| WindowsAsidePanelEvent::TabClose { panel_id, .. }
| WindowsAsidePanelEvent::CloseAll { panel_id }
| WindowsAsidePanelEvent::NavBack { panel_id }
| WindowsAsidePanelEvent::NavForward { panel_id }
| WindowsAsidePanelEvent::NavReload { panel_id } => panel_id,
};
if panel_id != ASIDE_BROWSER_PANEL_ID {
if let Some(handler) = MANAGED_ASIDE_EVENT_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
{
handler(event);
}
return;
}
match event {
WindowsAsidePanelEvent::TabClick { surface_id, .. } => {
{
let mut group = aside_browser_group();
if !group.order.contains(&surface_id) {
return;
}
group.active = Some(surface_id);
}
sync_aside_browser_group();
}
WindowsAsidePanelEvent::TabClose { surface_id, .. } => close_aside_tab(&surface_id),
WindowsAsidePanelEvent::CloseAll { .. } => {
let order = aside_browser_group().order.clone();
for id in order {
close_aside_tab(&id);
}
}
WindowsAsidePanelEvent::NavBack { .. } => {
with_active_aside_webview(|webview| webview.go_back())
}
WindowsAsidePanelEvent::NavForward { .. } => {
with_active_aside_webview(|webview| webview.go_forward())
}
WindowsAsidePanelEvent::NavReload { .. } => {
with_active_aside_webview(|webview| webview.reload())
}
}
}
fn close_aside_tab(id: &str) {
{
let mut group = aside_browser_group();
group.order.retain(|existing| existing != id);
if group.active.as_deref() == Some(id) {
group.active = group.order.last().cloned();
}
}
if let Some(entry) = SURFACES.lock().ok().and_then(|mut map| map.remove(id)) {
teardown_surface(&entry, id, "user");
} else {
notify_surface_closed(id, "user");
}
sync_aside_browser_group();
}
fn with_active_aside_webview(
action: impl FnOnce(&lingxia_webview::WebView) -> Result<(), lingxia_webview::WebViewError>,
) {
let active = aside_browser_group().active.clone();
let Some(entry) = active.as_deref().and_then(surface_entry) else {
return;
};
let Some(webview) = webview_runtime::find_webview(&entry.webtag) else {
return;
};
if let Err(err) = action(&webview) {
log::warn!("aside browser navigation failed: {err}");
}
}
pub(super) fn present_surface(
request: SurfaceRequest,
product_name: &str,
) -> Result<(), PlatformError> {
let mut cleanup = None;
let webtag = if request.content == SurfaceContent::Url {
if let Some(resolved) = resolve_url_surface(&request) {
cleanup = resolved.cleanup;
WebTag::new(&resolved.app_id, &resolved.path, Some(resolved.session_id))
} else {
WebTag::new(&request.app_id, &request.path, Some(request.session_id))
}
} else if request.page_instance_id.is_empty() {
WebTag::new(&request.app_id, &request.path, Some(request.session_id))
} else {
WebTag::new(
&request.app_id,
&format!("{}#{}", request.path, request.page_instance_id),
Some(request.session_id),
)
};
let id = request.id.clone();
let kind = request.kind;
let title = if request.content == SurfaceContent::Url {
request.path.clone()
} else {
product_name.to_string()
};
let page_instance_id =
(!request.page_instance_id.is_empty()).then(|| request.page_instance_id.clone());
let placement = OverlayPlacement {
width: finite_or_zero(request.width),
height: finite_or_zero(request.height),
width_ratio: finite_or_zero(request.width_ratio),
height_ratio: finite_or_zero(request.height_ratio),
position: request.position as u8,
};
let is_web = request.content == SurfaceContent::Url;
log::info!(
"windows surface present: id={} role={:?} kind={:?} web={} close_button={} dismiss={:?} modal={} path={}",
request.id,
request.role,
request.kind,
is_web,
request.interaction.close_button,
request.interaction.dismiss,
request.interaction.modal,
request.path
);
if let Ok(mut map) = SURFACES.lock() {
map.insert(
id.clone(),
SurfaceEntry {
webtag: webtag.clone(),
kind,
role: request.role,
title: title.clone(),
page_instance_id,
cleanup,
placement,
is_web,
interaction: request.interaction,
},
);
}
if request.role == SurfaceRole::Aside && is_web {
aside_browser_group().active = Some(id.clone());
}
if request.role == SurfaceRole::Float || (request.role == SurfaceRole::Aside && !is_web) {
let close_id = id.clone();
set_webview_close_handler(
&webtag,
Arc::new(move || {
let _ = close_surface("", &close_id, "user");
}),
);
}
if request.role != SurfaceRole::Aside {
present_surface_when_ready(webtag, id, PresentationTarget::Stored);
}
Ok(())
}
pub(super) fn present_layout(
_window_id: &str,
plan: &LayoutPresentationPlan,
_product_name: &str,
) -> Result<(), PlatformError> {
if let Ok(mut last) = LAST_LAYOUT_PLAN.lock() {
*last = Some(plan.clone());
}
let known = SURFACES
.lock()
.ok()
.map(|map| map.clone())
.unwrap_or_default();
log::info!(
"windows surface layout: main={:?} asides={:?} floats={}",
plan.active_main_id,
plan.asides
.iter()
.map(|aside| &aside.id)
.collect::<Vec<_>>(),
plan.floats.len()
);
if let Some(active_main_id) = plan.active_main_id.as_deref()
&& let Some(entry) = known.get(active_main_id)
{
present_surface_when_ready(
entry.webtag.clone(),
active_main_id.to_string(),
PresentationTarget::Stored,
);
}
let aside_by_id: HashMap<_, _> = plan
.asides
.iter()
.map(|aside| (aside.id.as_str(), aside))
.collect();
let desired_aside_ids: Vec<&str> = if plan.aside_slots.is_empty() {
plan.asides.iter().map(|aside| aside.id.as_str()).collect()
} else {
plan.aside_slots
.iter()
.filter(|slot| slot.visible)
.flat_map(|slot| {
if slot.kind == lingxia_surface::SlotKind::Browser {
slot.children.iter().map(String::as_str).collect::<Vec<_>>()
} else {
slot.active_child
.as_deref()
.or_else(|| slot.children.last().map(String::as_str))
.into_iter()
.collect()
}
})
.collect()
};
let overlay_ids: HashSet<&str> = plan
.aside_slots
.iter()
.filter(|slot| slot.visible && slot.overlay)
.filter_map(|slot| {
slot.active_child
.as_deref()
.or_else(|| slot.children.last().map(String::as_str))
})
.collect();
let mut planned_asides = HashSet::new();
let mut planned_web_asides = Vec::new();
for id in desired_aside_ids {
let Some(aside) = aside_by_id.get(id).copied() else {
continue;
};
planned_asides.insert(aside.id.clone());
let Some(entry) = known.get(&aside.id) else {
continue;
};
if entry.is_web {
planned_web_asides.push((aside.id.clone(), aside.edge, aside.preferred_size));
continue;
}
let target = if overlay_ids.contains(aside.id.as_str()) {
let _ = hide_host_panel(&aside.id);
PresentationTarget::Overlay
} else {
PresentationTarget::Aside {
edge: aside.edge,
preferred_size: aside.preferred_size,
grouped: false,
}
};
present_surface_when_ready(entry.webtag.clone(), aside.id.clone(), target);
}
let active_browser = plan
.aside_slots
.iter()
.find(|slot| slot.kind == lingxia_surface::SlotKind::Browser && slot.visible)
.and_then(|slot| slot.active_child.as_deref());
let browser_overlay = plan
.aside_slots
.iter()
.find(|slot| slot.kind == lingxia_surface::SlotKind::Browser && slot.visible)
.is_some_and(|slot| slot.overlay);
store_aside_browser_plan(planned_web_asides, active_browser, browser_overlay);
sync_aside_browser_group();
sync_runtime_lxapp_asides(plan);
let planned_floats: HashSet<_> = plan.floats.iter().map(|float| float.id.clone()).collect();
for float_id in &planned_floats {
if let Some(entry) = known.get(float_id) {
present_surface_when_ready(
entry.webtag.clone(),
float_id.clone(),
PresentationTarget::Stored,
);
}
}
for (id, entry) in known {
let still_planned = match entry.role {
SurfaceRole::Aside => planned_asides.contains(&id),
SurfaceRole::Float => planned_floats.contains(&id),
SurfaceRole::Main => true,
};
if !still_planned {
hide_entry(&entry);
}
}
if let Some(handler) = LAYOUT_PLAN_HANDLER
.lock()
.ok()
.and_then(|slot| slot.clone())
{
handler(plan);
}
Ok(())
}
pub(super) fn close_surface(_app_id: &str, id: &str, reason: &str) -> Result<(), PlatformError> {
if let Some(entry) = SURFACES.lock().ok().and_then(|mut map| map.remove(id)) {
teardown_surface(&entry, id, reason);
} else {
notify_surface_closed(id, reason);
}
Ok(())
}
pub(super) fn show_surface(_app_id: &str, id: &str) -> Result<(), PlatformError> {
let Some(entry) = surface_entry(id) else {
return Err(PlatformError::InvalidParameter(format!(
"unknown surface: {id}"
)));
};
if find_webview_handler(&entry.webtag).is_none() {
present_surface_when_ready(entry.webtag, id.to_string(), PresentationTarget::Stored);
return Ok(());
}
let result = present_entry(id, &entry, PresentationTarget::Stored);
if result.is_ok()
&& let Some(page_instance_id) = &entry.page_instance_id
{
notify_page_visibility_when_ready(page_instance_id.clone(), true);
}
result.map_err(|err| PlatformError::Platform(format!("failed to show surface {id}: {err}")))
}
pub(super) fn hide_surface(_app_id: &str, id: &str) -> Result<(), PlatformError> {
let Some(entry) = surface_entry(id) else {
return Err(PlatformError::InvalidParameter(format!(
"unknown surface: {id}"
)));
};
hide_entry(&entry);
Ok(())
}
fn present_surface_when_ready(webtag: WebTag, id: String, target: PresentationTarget) {
if find_webview_handler(&webtag).is_some() {
present_surface_with_handler(&webtag, &id, target);
return;
}
let _ = thread::Builder::new()
.name(format!("lingxia-surface-{}", webtag.key()))
.spawn(move || {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if surface_entry(&id).is_none() {
return;
}
if find_webview_handler(&webtag).is_some() {
present_surface_with_handler(&webtag, &id, target);
return;
}
thread::sleep(Duration::from_millis(50));
}
log::error!("Timed out waiting for surface WebView {}", webtag.key());
});
}
fn present_surface_with_handler(webtag: &WebTag, id: &str, target: PresentationTarget) {
let id_for_close = id.to_string();
set_webview_close_handler(
webtag,
Arc::new(move || {
if let Some(entry) = SURFACES
.lock()
.ok()
.and_then(|mut map| map.remove(&id_for_close))
{
teardown_surface(&entry, &id_for_close, "user");
} else {
notify_surface_closed(&id_for_close, "user");
}
}),
);
let Some(entry) = surface_entry(id) else {
return;
};
if let Err(err) = present_entry(id, &entry, target) {
log::warn!("Failed to present surface {}: {}", webtag.key(), err);
return;
}
if let Some(page_instance_id) = surface_entry(id).and_then(|entry| entry.page_instance_id) {
notify_page_visibility_when_ready(page_instance_id, true);
}
}