lingxia-lxapp 0.4.3

LxApp (lightweight application) container and runtime for LingXia framework
use lingxia_platform::traits::app_runtime::{AppRuntime, OpenUrlRequest, OpenUrlTarget};
use lingxia_platform::traits::ui::{PopupPresenter, PopupRequest};
use lingxia_webview::WebViewController;
use lingxia_webview::runtime::destroy_webview;
use lingxia_webview::{NavigationPolicy, NewWindowPolicy, WebTag, WebViewBuilder};
use std::sync::Arc;

use crate::PageLifecycleEvent;
use crate::error::LxAppError;
use crate::lxapp::LxApp;

/// Fixed WebTag path used for the web-page popup WebView.
/// Safe to be a constant because only one popup can be active at a time.
pub(crate) const WEB_POPUP_PATH: &str = "__web_popup__";

/// Controls what content is loaded in the popup.
/// Both modes display an in-app popup overlay; they differ only in content source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PopupMode {
    /// Render an internal lxapp page (`pages/...` or plugin route) — existing behavior.
    LxAppPage,
    /// Load a standard http/https URL in a browser-profile WebView.
    WebPage,
}

/// Tracks what kind of popup is currently active.
#[derive(Debug, Clone)]
pub(crate) enum ActivePopup {
    /// An internal lxapp page popup; holds the resolved page path.
    LxAppPage(String),
    /// A browser-profile WebView popup loading an https URL.
    WebPage,
}

impl LxApp {
    fn create_web_popup_webview(self: &Arc<Self>, target_url: &str) -> Result<(), LxAppError> {
        let owner_appid_for_nav = self.appid.clone();
        let owner_session_for_nav = self.session_id();
        let runtime_for_nav = self.runtime.clone();
        let owner_appid_for_ready = self.appid.clone();
        let owner_session_for_ready = self.session_id();
        let target_url_for_ready = target_url.to_string();

        let webtag = WebTag::new(&self.appid, WEB_POPUP_PATH, Some(self.session_id()));
        let session = WebViewBuilder::browser(webtag)
            .on_navigation(move |url| {
                let decision = crate::browser::handle_browser_navigation_policy(
                    crate::browser::BrowserNavigationPolicyRequest {
                        raw_url: url.to_string(),
                        has_user_gesture: true,
                        is_main_frame: true,
                    },
                );
                match decision.decision {
                    crate::browser::BrowserNavigationPolicyDecision::InWebview => {
                        NavigationPolicy::Allow
                    }
                    crate::browser::BrowserNavigationPolicyDecision::OpenExternal => {
                        let _ = runtime_for_nav.open_url(OpenUrlRequest {
                            owner_appid: owner_appid_for_nav.clone(),
                            owner_session_id: owner_session_for_nav,
                            url: url.to_string(),
                            target: OpenUrlTarget::External,
                        });
                        NavigationPolicy::Cancel
                    }
                    crate::browser::BrowserNavigationPolicyDecision::Deny => {
                        NavigationPolicy::Cancel
                    }
                }
            })
            .on_new_window(move |url| {
                let _ = url;
                NewWindowPolicy::LoadInSelf
            })
            .create();

        if let Err(e) = rong::bg::spawn(async move {
            match session.wait_ready().await {
                Ok(webview) => {
                    if let Err(load_err) = webview.load_url(&target_url_for_ready) {
                        crate::warn!(
                            "[Popup] failed to load external url url={} err={}",
                            target_url_for_ready,
                            load_err
                        );
                    }
                }
                Err(wait_err) => {
                    crate::warn!(
                        "[Popup] failed to create external popup webview err={}",
                        wait_err
                    );
                    destroy_webview(&WebTag::new(
                        &owner_appid_for_ready,
                        WEB_POPUP_PATH,
                        Some(owner_session_for_ready),
                    ));
                }
            }
        }) {
            return Err(LxAppError::Runtime(format!(
                "failed to spawn external popup webview task: {}",
                e
            )));
        }

        Ok(())
    }

    /// Show a popup with explicit mode control.
    ///
    /// - `LxAppPage`: resolves an lxapp route, creates/reuses its Page, dispatches lifecycle.
    /// - `WebPage`: validates the URL is http/https, creates a browser-profile WebView popup.
    pub fn show_popup_with_mode(
        self: &Arc<Self>,
        mode: PopupMode,
        mut request: PopupRequest,
    ) -> Result<(), LxAppError> {
        self.hide_popup()?;
        request.app_id = self.appid.clone();

        if !request.width_ratio.is_nan() {
            request.width_ratio = request.width_ratio.clamp(0.0, 1.0);
        }
        if !request.height_ratio.is_nan() {
            request.height_ratio = request.height_ratio.clamp(0.0, 1.0);
        }

        let active = match mode {
            PopupMode::LxAppPage => {
                let resolved = crate::route::resolve_route(self, &request.path)?;
                let path = resolved.internal_path();
                let query_str = resolved.query.unwrap_or_default();

                let popup_page = self.get_or_create_page(&path);
                popup_page.mark_active();

                if !query_str.is_empty() {
                    popup_page.set_query(query_str);
                }

                popup_page.dispatch_lifecycle_event(PageLifecycleEvent::OnLoad);
                request.path = path.clone();
                ActivePopup::LxAppPage(path)
            }
            PopupMode::WebPage => {
                let target_url = crate::browser::normalize_browser_target_url(&request.path);
                let scheme = crate::browser::extract_url_scheme(&target_url);
                if !matches!(scheme.as_deref(), Some("http" | "https")) {
                    return Err(LxAppError::InvalidParameter(format!(
                        "WebPage popup only supports http/https URLs, got '{}'",
                        request.path
                    )));
                }

                self.create_web_popup_webview(&target_url)?;
                request.path = WEB_POPUP_PATH.to_string();
                ActivePopup::WebPage
            }
        };

        self.runtime.show_popup(request).map_err(LxAppError::from)?;
        if let Ok(mut state) = self.state.lock() {
            state.current_popup = Some(active);
        }

        Ok(())
    }

    /// Show popup content rendered via WebView (internal lxapp page).
    pub fn show_popup(self: &Arc<Self>, request: PopupRequest) -> Result<(), LxAppError> {
        self.show_popup_with_mode(PopupMode::LxAppPage, request)
    }

    /// Hide the currently displayed popup, if any.
    pub fn hide_popup(self: &Arc<Self>) -> Result<(), LxAppError> {
        let active = {
            let mut state = self.state.lock().unwrap();
            state.current_popup.take()
        };

        let Some(active) = active else {
            return Ok(());
        };

        if let ActivePopup::LxAppPage(ref path) = active {
            if let Some(page) = self.get_page(path) {
                page.dispatch_lifecycle_event(PageLifecycleEvent::OnHide);
                page.dispatch_lifecycle_event(PageLifecycleEvent::OnUnload);
            }
        }

        self.runtime
            .hide_popup(&self.appid)
            .map_err(LxAppError::from)?;

        if let ActivePopup::WebPage = active {
            destroy_webview(&WebTag::new(
                &self.appid,
                WEB_POPUP_PATH,
                Some(self.session.id),
            ));
        }

        Ok(())
    }
}