pub(crate) mod config;
pub(crate) mod definition;
pub(crate) mod runtime;
pub use definition::{register_page_resolver, resolve_page_path};
use crate::bridge::{IncomingMessage, PageBridge};
use crate::lifecycle::PageLifecycleEvent;
use crate::lxapp::{self, navbar::NavigationBarState};
use crate::page::config::{OrientationOverride, PageConfig};
use crate::plugin;
use crate::startup::parse_query_string;
use crate::{LxApp, LxAppError, error, info};
use base64::Engine;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use lingxia_log::{LogBuilder, LogLevel as LxLogLevel, LogTag};
use lingxia_platform::traits::app_runtime::{
AnimationType, AppRuntime, OpenUrlRequest, OpenUrlTarget,
};
use lingxia_webview::runtime::destroy_webview;
use lingxia_webview::{
LoadDataRequest, LogLevel, NavigationPolicy, NewWindowPolicy, WebTag, WebView, WebViewBuilder,
WebViewController, WebViewDelegate,
};
use ring::rand::{SecureRandom, SystemRandom};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use tokio::sync::watch;
static GLOBAL_PAGE_SCRIPTS: OnceLock<Mutex<Vec<Arc<str>>>> = OnceLock::new();
pub fn add_global_page_script(js: impl Into<String>) {
let scripts = GLOBAL_PAGE_SCRIPTS.get_or_init(|| Mutex::new(Vec::new()));
if let Ok(mut guard) = scripts.lock() {
guard.push(Arc::from(js.into()));
}
}
pub(crate) fn global_page_scripts_snapshot() -> Vec<Arc<str>> {
GLOBAL_PAGE_SCRIPTS
.get()
.and_then(|m| m.lock().ok())
.map(|guard| guard.clone())
.unwrap_or_default()
}
type WebviewReadyReceiver = Arc<Mutex<watch::Receiver<Option<Result<(), String>>>>>;
const DEFAULT_VIEW_CALL_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Clone)]
pub(crate) struct PageInstanceInner {
id: PageInstanceId,
appid: String,
path: String,
webtag: WebTag,
webview: Arc<Mutex<Option<Arc<WebView>>>>,
last_active_time: Arc<Mutex<Instant>>,
state: Arc<Mutex<PageState>>,
bridge_nonce: Arc<Mutex<Option<String>>>,
bridge: PageBridge,
webview_ready_tx: watch::Sender<Option<Result<(), String>>>,
webview_ready_rx: WebviewReadyReceiver,
page_scripts: Vec<Arc<str>>,
loaded_tx: watch::Sender<u64>,
}
#[derive(Clone, Debug)]
pub struct PageState {
render_status: PageRenderStatus,
event: PageLifecycleEvent,
show_requested: bool,
on_load_fired: bool,
on_show_fired: bool,
on_ready_fired: bool,
pub(crate) navbar_state: NavigationBarState,
pub(crate) enable_pull_down_refresh: bool,
pub(crate) orientation_override: OrientationOverride,
pub(crate) query: serde_json::Value,
}
#[derive(Copy, Clone, PartialEq, Debug)]
enum PageRenderStatus {
Unstarted,
Started,
Finished,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavigationType {
Launch = 0,
Forward = 1,
Backward = 2,
Replace = 3,
SwitchTab = 4,
}
impl NavigationType {
pub fn to_animation(self) -> AnimationType {
match self {
NavigationType::Forward => AnimationType::Forward,
NavigationType::Backward => AnimationType::Backward,
_ => AnimationType::None,
}
}
}
#[derive(Clone)]
pub struct PageInstance {
inner: Arc<PageInstanceInner>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PageInstanceId(String);
pub(crate) enum WebTagInstance {
PageInstanceId,
}
impl PageInstanceId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn parse(raw: impl Into<String>) -> Option<Self> {
let value = raw.into();
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
uuid::Uuid::parse_str(trimmed)
.ok()
.map(|id| Self(id.hyphenated().to_string()))
}
}
impl Default for PageInstanceId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for PageInstanceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ViewCallOptions {
timeout: Duration,
}
impl Default for ViewCallOptions {
fn default() -> Self {
Self {
timeout: DEFAULT_VIEW_CALL_TIMEOUT,
}
}
}
impl ViewCallOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn timeout(self) -> Duration {
self.timeout
}
}
fn serialize_view_call_params<P>(params: &P) -> Result<Option<Value>, LxAppError>
where
P: Serialize + ?Sized,
{
let value = serde_json::to_value(params)?;
if value.is_null() {
return Ok(None);
}
Ok(Some(value))
}
fn decode_view_call_result<R>(method: &str, value: Value) -> Result<R, LxAppError>
where
R: DeserializeOwned,
{
serde_json::from_value(value).map_err(|err| {
LxAppError::Bridge(format!(
"Failed to decode view response for '{}': {}",
method, err
))
})
}
impl PageInstance {
pub(crate) fn from_inner(inner: Arc<PageInstanceInner>) -> Self {
Self { inner }
}
fn generate_bridge_nonce() -> String {
let rng = SystemRandom::new();
let mut bytes = [0u8; 16];
if rng.fill(&mut bytes).is_err() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_le_bytes();
bytes.copy_from_slice(&nanos[..16]);
}
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
fn build_page_state(lxapp: &lxapp::LxApp, path: &str) -> PageState {
let page_config = if lxapp.logic_enabled() {
PageConfig::from_json(lxapp, path)
} else {
PageConfig::default()
};
PageState {
event: PageLifecycleEvent::Unknown,
render_status: PageRenderStatus::Unstarted,
show_requested: false,
on_load_fired: false,
on_show_fired: false,
on_ready_fired: false,
navbar_state: page_config.create_navbar_state(),
enable_pull_down_refresh: page_config.is_pull_down_refresh_enabled(),
orientation_override: page_config.get_orientation_override(),
query: serde_json::Value::Null,
}
}
pub(crate) fn new<F, Fut>(appid: String, path: String, lxapp: &LxApp, setup_callback: F) -> Self
where
F: Fn(&PageInstance) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<(), String>> + Send + 'static,
{
Self::new_with_webtag_instance(appid, path, lxapp, None, setup_callback)
}
pub(crate) fn new_with_webtag_instance<F, Fut>(
appid: String,
path: String,
lxapp: &LxApp,
webtag_instance: Option<WebTagInstance>,
setup_callback: F,
) -> Self
where
F: Fn(&PageInstance) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<(), String>> + Send + 'static,
{
let page_state = Self::build_page_state(lxapp, &path);
let id = PageInstanceId::new();
let webtag = webtag_instance
.as_ref()
.map(|instance| {
let instance_id = match instance {
WebTagInstance::PageInstanceId => id.as_str(),
};
WebTag::new(
&appid,
&format!("{path}#{instance_id}"),
Some(lxapp.session.id),
)
})
.unwrap_or_else(|| WebTag::new(&appid, &path, Some(lxapp.session.id)));
let bridge_nonce = Self::generate_bridge_nonce();
let lxapp_arc = lxapp.clone_arc();
let (ready_tx, ready_rx) = watch::channel(None);
let (loaded_tx, _) = watch::channel(0u64);
let inner = Arc::new(PageInstanceInner {
id,
appid: appid.clone(),
path: path.clone(),
webtag: webtag.clone(),
last_active_time: Arc::new(Mutex::new(Instant::now())),
state: Arc::new(Mutex::new(page_state)),
webview: Arc::new(Mutex::new(None)),
bridge_nonce: Arc::new(Mutex::new(Some(bridge_nonce))),
bridge: PageBridge::new(lxapp_arc.clone(), lxapp_arc.executor.clone()),
webview_ready_tx: ready_tx,
webview_ready_rx: Arc::new(Mutex::new(ready_rx)),
page_scripts: lxapp.page_scripts_snapshot(),
loaded_tx,
});
let page_weak_for_lx = Arc::downgrade(&inner);
let page = Self { inner };
let appid_for_lx = appid.clone();
let runtime_for_nav = lxapp.runtime.clone();
let appid_for_nav = appid.clone();
let session_id_for_nav = lxapp.session_id();
let runtime_for_new_window = lxapp.runtime.clone();
let appid_for_new_window = appid.clone();
let session_id_for_new_window = lxapp.session_id();
let session = WebViewBuilder::strict(webtag)
.delegate(Arc::new(page.clone()))
.on_scheme("lx", move |req| {
let page_weak_for_lx = page_weak_for_lx.clone();
let appid_for_lx = appid_for_lx.clone();
async move {
let Some(inner) = page_weak_for_lx.upgrade() else {
return None.into();
};
let lxapp = lxapp::get(appid_for_lx);
let page = PageInstance::from_inner(inner);
lxapp.handle_lingxia_request(&page, req).into()
}
})
.on_navigation(move |url| {
let scheme = url.split(':').next().unwrap_or("");
match scheme {
"lx" | "data" | "blob" => NavigationPolicy::Allow,
_ => {
if scheme != "about" {
let _ = runtime_for_nav.open_url(OpenUrlRequest {
owner_appid: appid_for_nav.clone(),
owner_session_id: session_id_for_nav,
url: url.to_string(),
target: OpenUrlTarget::External,
});
}
NavigationPolicy::Cancel
}
}
})
.on_new_window(move |url| {
let _ = runtime_for_new_window.open_url(OpenUrlRequest {
owner_appid: appid_for_new_window.clone(),
owner_session_id: session_id_for_new_window,
url: url.to_string(),
target: OpenUrlTarget::SelfTarget,
});
NewWindowPolicy::Cancel
})
.create();
let page_for_task = page.clone();
let appid_clone = appid.clone();
let path_clone = path.clone();
crate::executor::spawn(async move {
match session.wait_ready().await {
Ok(webview_controller) => {
page_for_task.attach_webview(webview_controller.clone());
let result = setup_callback(&page_for_task).await;
page_for_task.mark_webview_ready(result);
}
Err(e) => {
error!("Failed to create WebView: {}", e)
.with_appid(appid_clone)
.with_path(path_clone);
page_for_task.mark_webview_ready(Err(e.to_string()));
}
}
});
page
}
pub(crate) fn new_headless(appid: String, path: String, lxapp: &LxApp) -> Self {
let page_state = Self::build_page_state(lxapp, &path);
let id = PageInstanceId::new();
let bridge_nonce = Self::generate_bridge_nonce();
let webtag = WebTag::new(&appid, &path, Some(lxapp.session.id));
let lxapp_arc = lxapp.clone_arc();
let (ready_tx, ready_rx) = watch::channel(None);
let (loaded_tx, _) = watch::channel(0u64);
let inner = Arc::new(PageInstanceInner {
id,
appid,
path,
webtag,
last_active_time: Arc::new(Mutex::new(Instant::now())),
state: Arc::new(Mutex::new(page_state)),
webview: Arc::new(Mutex::new(None)),
bridge_nonce: Arc::new(Mutex::new(Some(bridge_nonce))),
bridge: PageBridge::new(lxapp_arc.clone(), lxapp_arc.executor.clone()),
webview_ready_tx: ready_tx,
webview_ready_rx: Arc::new(Mutex::new(ready_rx)),
page_scripts: lxapp.page_scripts_snapshot(),
loaded_tx,
});
Self { inner }
}
pub fn bridge_nonce(&self) -> Option<String> {
self.inner.bridge_nonce.lock().ok().and_then(|v| v.clone())
}
pub fn instance_id(&self) -> PageInstanceId {
self.inner.id.clone()
}
pub fn instance_id_string(&self) -> String {
self.inner.id.to_string()
}
pub(crate) fn webtag(&self) -> WebTag {
self.inner.webtag.clone()
}
pub(crate) fn bridge(&self) -> PageBridge {
self.inner.bridge.clone()
}
pub fn attach_webview(&self, webview: Arc<WebView>) {
if let Ok(mut webview_guard) = self.inner.webview.lock() {
*webview_guard = Some(webview);
}
}
pub fn handle_incoming_message_json(&self, msg: &str) -> Result<(), LxAppError> {
let incoming = IncomingMessage::from_json_str(msg)
.map_err(|err| LxAppError::Bridge(format!("Invalid bridge message JSON: {}", err)))?;
self.inner.bridge.handle_incoming(self, Arc::new(incoming))
}
pub fn get_page_state(&self) -> Option<PageState> {
self.inner.state.lock().ok().map(|state| state.clone())
}
fn set_render_status(&self, status: PageRenderStatus) {
if let Ok(mut state) = self.inner.state.lock() {
state.render_status = status;
}
}
pub(crate) fn dispatch_lifecycle_event(&self, event: PageLifecycleEvent) {
let mut events_to_fire: Vec<(PageLifecycleEvent, Option<String>)> = Vec::new();
{
let mut state = self.inner.state.lock().unwrap();
if event == PageLifecycleEvent::OnPullDownRefresh {
events_to_fire.push((event, None));
}
else if event == PageLifecycleEvent::OnHide || event == PageLifecycleEvent::OnUnload {
if state.event != event {
events_to_fire.push((event, None));
state.event = event;
state.on_show_fired = false;
}
} else {
if event == PageLifecycleEvent::OnShow {
state.show_requested = true;
}
if event == PageLifecycleEvent::OnLoad
&& matches!(state.render_status, PageRenderStatus::Unstarted)
{
return;
}
if event == PageLifecycleEvent::OnLoad {
let query = serde_json::to_string(&state.query).ok();
events_to_fire.push((PageLifecycleEvent::OnLoad, query));
state.on_load_fired = true;
state.on_show_fired = false;
state.on_ready_fired = false;
}
if state.on_load_fired
&& state.render_status == PageRenderStatus::Finished
&& !state.on_ready_fired
{
events_to_fire.push((PageLifecycleEvent::OnReady, None));
state.on_ready_fired = true;
}
if state.on_load_fired && state.show_requested && !state.on_show_fired {
events_to_fire.push((PageLifecycleEvent::OnShow, None));
state.on_show_fired = true;
state.event = PageLifecycleEvent::OnShow;
}
}
}
if !events_to_fire.is_empty() {
let lxapp = lxapp::get(self.inner.appid.clone());
let appid = self.appid();
let path = self.path();
for (event, query) in events_to_fire {
let page_event = match event {
PageLifecycleEvent::OnLoad => crate::lifecycle::PageServiceEvent::OnLoad,
PageLifecycleEvent::OnShow => crate::lifecycle::PageServiceEvent::OnShow,
PageLifecycleEvent::OnReady => crate::lifecycle::PageServiceEvent::OnReady,
PageLifecycleEvent::OnHide => crate::lifecycle::PageServiceEvent::OnHide,
PageLifecycleEvent::OnUnload => crate::lifecycle::PageServiceEvent::OnUnload,
PageLifecycleEvent::OnPullDownRefresh => {
crate::lifecycle::PageServiceEvent::OnPullDownRefresh
}
PageLifecycleEvent::Unknown => {
continue;
}
};
if let Err(e) = lxapp.executor.call_page_service_event(
lxapp.clone(),
path.clone(),
Some(self.instance_id_string()),
page_event,
query,
) {
error!("Failed to call {}: {}", String::from(event), e)
.with_appid(appid.clone())
.with_path(path.clone());
}
}
}
}
pub fn get_navbar_state(&self) -> Option<NavigationBarState> {
self.inner
.state
.lock()
.ok()
.map(|state| state.navbar_state.clone())
}
pub fn get_navbar_state_mut<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce(&mut NavigationBarState) -> R,
{
self.inner
.state
.lock()
.ok()
.map(|mut state| f(&mut state.navbar_state))
}
pub fn get_orientation_override(&self) -> Option<OrientationOverride> {
self.inner
.state
.lock()
.ok()
.map(|state| state.orientation_override)
}
pub fn webview(&self) -> Option<Arc<WebView>> {
if let Ok(webview_guard) = self.inner.webview.lock() {
webview_guard.clone()
} else {
None
}
}
pub(crate) fn mark_webview_ready(&self, result: Result<(), String>) {
let _ = self.inner.webview_ready_tx.send(Some(result));
}
pub fn notify_page_started(&self) {
self.set_render_status(PageRenderStatus::Started);
}
pub async fn wait_webview_ready(&self) -> Result<(), String> {
let rx = {
self.inner
.webview_ready_rx
.lock()
.map(|r| r.clone())
.map_err(|_| "webview ready receiver poisoned".to_string())?
};
if let Some(res) = rx.borrow().clone() {
return res;
}
let mut rx = rx;
while rx.changed().await.is_ok() {
if let Some(res) = rx.borrow().clone() {
return res;
}
}
Err("webview ready channel closed before result".to_string())
}
async fn handle_loaded_async(&self) {
self.set_render_status(PageRenderStatus::Finished);
if !self.inner.page_scripts.is_empty() {
if let Some(webview) = self.webview() {
for js in &self.inner.page_scripts {
if let Err(e) = webview.exec_js(js) {
crate::error!("page script injection failed: {}", e)
.with_appid(self.inner.appid.clone())
.with_path(self.inner.path.clone());
}
}
}
}
self.dispatch_lifecycle_event(PageLifecycleEvent::OnReady);
self.inner.loaded_tx.send_modify(|v| *v = v.wrapping_add(1));
}
pub fn handle_loaded(&self) {
let page = self.clone();
let _ = crate::executor::spawn(async move {
page.handle_loaded_async().await;
});
}
pub fn subscribe_loaded(&self) -> watch::Receiver<u64> {
self.inner.loaded_tx.subscribe()
}
pub fn detach_webview(&self) {
if let Ok(mut webview_guard) = self.inner.webview.lock() {
let _ = webview_guard.take();
}
}
pub(crate) fn webview_controller(&self) -> Option<Arc<dyn WebViewController>> {
if let Some(webview) = self.webview() {
Some(webview as Arc<dyn WebViewController>)
} else {
None
}
}
pub(crate) fn load_html(&self) -> Result<(), LxAppError> {
let lxapp = lxapp::get(self.appid());
let path = self.path();
let html_data = lxapp.generate_page_html(&path, self.bridge_nonce().as_deref());
let base_url = self.base_url();
let html_string = String::from_utf8_lossy(&html_data).into_owned();
if let Some(controller) = self.webview_controller() {
controller
.load_data(LoadDataRequest::new(&html_string, &base_url))
.map_err(|e| LxAppError::WebView(e.to_string()))
} else {
Err(LxAppError::WebView("WebView not ready".to_string()))
}
}
pub fn path(&self) -> String {
self.inner.path.clone()
}
pub fn appid(&self) -> String {
self.inner.appid.clone()
}
pub fn base_url(&self) -> String {
if let Some((plugin_name, page_path)) = plugin::parse_plugin_page_path(&self.path()) {
if page_path.is_empty() {
return format!("lx://plugin/{}", plugin_name);
}
return format!("lx://plugin/{}/{}", plugin_name, page_path);
}
format!("lx://lxapp/{}/{}", self.appid(), self.path())
}
pub(crate) fn mark_active(&self) {
if let Ok(mut time) = self.inner.last_active_time.lock() {
*time = Instant::now();
}
}
pub(crate) fn get_last_active_time(&self) -> Option<Instant> {
self.inner.last_active_time.lock().ok().map(|time| *time)
}
pub fn is_pull_down_refresh_enabled(&self) -> bool {
self.inner
.state
.lock()
.ok()
.map(|state| state.enable_pull_down_refresh)
.unwrap_or(false)
}
pub fn is_tabbar_page(&self) -> bool {
let lxapp = lxapp::get(self.inner.appid.clone());
match lxapp.get_tabbar() {
Some(tab_bar) => tab_bar.is_tabbar_page(&self.inner.path),
None => false,
}
}
pub fn navigate_to(
&self,
target_page: PageInstance,
nav_type: NavigationType,
) -> Result<PageInstance, LxAppError> {
let lxapp = lxapp::get(self.appid());
let target_page = lxapp.get_or_create_page(&target_page.path());
self.navigate_to_internal(target_page, nav_type, &lxapp)
}
fn navigate_to_internal(
&self,
target_page: PageInstance,
nav_type: NavigationType,
lxapp: &Arc<LxApp>,
) -> Result<PageInstance, LxAppError> {
let path = target_page.path();
let mut target_page = target_page;
let is_tabbar_page = lxapp
.get_tabbar()
.map_or(false, |tabbar| tabbar.is_tabbar_page(&path));
let is_tab_switch = nav_type == NavigationType::SwitchTab
|| (nav_type == NavigationType::Launch && is_tabbar_page);
let is_initial_route = path == lxapp.config.get_initial_route();
match nav_type {
NavigationType::Launch | NavigationType::SwitchTab => {
if nav_type == NavigationType::Launch {
let stack_paths = lxapp.get_page_stack();
for stack_path in &stack_paths {
if let Some(page) = lxapp.get_page(stack_path) {
page.dispatch_lifecycle_event(PageLifecycleEvent::OnUnload);
page.detach_webview();
}
destroy_webview(&WebTag::new(
&lxapp.appid,
stack_path,
Some(lxapp.session.id),
));
}
lxapp.remove_pages(&stack_paths);
target_page = lxapp.get_or_create_page(&path);
}
lxapp.clear_page_stack()?;
}
NavigationType::Replace => {
lxapp.pop_from_page_stack();
}
NavigationType::Forward => {
if lxapp.is_page_stack_full() {
info!("PageInstance stack is full, cannot navigate forward.");
return Ok(target_page);
}
}
NavigationType::Backward => {
return Err(LxAppError::UnsupportedOperation(
"should use navigate_back".to_string(),
));
}
}
lxapp.with_tabbar_mut(|t| t.set_visible(is_tab_switch));
if is_tab_switch {
if let Some(Some(index)) = lxapp.with_tabbar_mut(|t| t.find_index_by_path(&path)) {
lxapp.with_tabbar_mut(|t| {
t.set_selected_index(index);
});
}
}
lxapp.push_to_page_stack(&path)?;
let stack_size = lxapp.get_page_stack_size();
let show_back_button = stack_size > 1;
let show_home_button = stack_size <= 1 && !is_tabbar_page && !is_initial_route;
target_page.get_navbar_state_mut(|navbar| {
let allow_buttons = navbar.show_navbar;
navbar.set_back_button_visibility(show_back_button && allow_buttons);
navbar.set_home_button_visibility(show_home_button && allow_buttons);
});
match nav_type {
NavigationType::Replace => {
self.dispatch_lifecycle_event(PageLifecycleEvent::OnUnload);
}
NavigationType::Launch => {}
_ => {
self.dispatch_lifecycle_event(PageLifecycleEvent::OnHide);
}
}
target_page.dispatch_lifecycle_event(PageLifecycleEvent::OnLoad);
(*lxapp.runtime)
.navigate(self.appid(), path, nav_type.to_animation())
.map_err(LxAppError::from)?;
Ok(target_page)
}
pub fn navigate_back(&self, delta: u32) -> Result<(), LxAppError> {
let lxapp = lxapp::get(self.appid());
let stack_size = lxapp.get_page_stack_size();
if stack_size <= 1 {
return Ok(());
}
let mut pages_to_pop = delta;
if pages_to_pop as usize >= stack_size {
pages_to_pop = (stack_size - 1) as u32;
}
if pages_to_pop == 0 {
return Ok(());
}
for _ in 0..pages_to_pop {
if let Some(path) = lxapp.pop_from_page_stack()
&& let Some(page) = lxapp.get_page(path.as_str())
{
page.dispatch_lifecycle_event(PageLifecycleEvent::OnUnload);
}
}
if let Some(path) = lxapp.peek_current_page() {
let is_tabbar_page = lxapp
.get_tabbar()
.is_some_and(|tabbar| tabbar.is_tabbar_page(&path));
lxapp.with_tabbar_mut(|t| t.set_visible(is_tabbar_page));
let new_stack_size = lxapp.get_page_stack_size();
if let Some(dest_page) = lxapp.get_page(&path) {
let is_initial_route = path == lxapp.config.get_initial_route();
let show_home_button = new_stack_size <= 1 && !is_tabbar_page && !is_initial_route;
dest_page.get_navbar_state_mut(|navbar| {
let allow_buttons = navbar.show_navbar;
navbar.set_back_button_visibility(new_stack_size > 1 && allow_buttons);
navbar.set_home_button_visibility(show_home_button && allow_buttons);
});
}
(*lxapp.runtime).navigate(
self.appid(),
path,
NavigationType::Backward.to_animation(),
)?;
Ok(())
} else {
Err(LxAppError::UnsupportedOperation(
"PageInstance stack is empty after pop".to_string(),
))
}
}
pub(crate) fn set_query(&self, query_str: String) {
if let Ok(query_value) = parse_query_string(&query_str) {
self.inner.state.lock().unwrap().query = query_value;
}
}
pub fn call_js(&self, name: String, arg: String) -> Result<(), LxAppError> {
let lxapp = lxapp::get(self.appid());
lxapp.executor.call_page_service(
lxapp.clone(),
self.path(),
Some(self.instance_id_string()),
name,
Some(arg),
)
}
pub async fn call_view<R>(&self, method: &str) -> Result<R, LxAppError>
where
R: DeserializeOwned,
{
self.call_view_in(method, ViewCallOptions::default()).await
}
pub async fn call_view_in<R>(
&self,
method: &str,
options: ViewCallOptions,
) -> Result<R, LxAppError>
where
R: DeserializeOwned,
{
let value = self.call_view_json_in(method, options).await?;
decode_view_call_result(method, value)
}
pub async fn call_view_with<P, R>(&self, method: &str, params: &P) -> Result<R, LxAppError>
where
P: Serialize + ?Sized,
R: DeserializeOwned,
{
self.call_view_with_in(method, params, ViewCallOptions::default())
.await
}
pub async fn call_view_with_in<P, R>(
&self,
method: &str,
params: &P,
options: ViewCallOptions,
) -> Result<R, LxAppError>
where
P: Serialize + ?Sized,
R: DeserializeOwned,
{
let value = self.call_view_json_with_in(method, params, options).await?;
decode_view_call_result(method, value)
}
pub async fn call_view_json(&self, method: &str) -> Result<Value, LxAppError> {
self.call_view_json_in(method, ViewCallOptions::default())
.await
}
pub async fn call_view_json_in(
&self,
method: &str,
options: ViewCallOptions,
) -> Result<Value, LxAppError> {
self.call_view_json_value(method, None, options).await
}
pub async fn call_view_json_with<P>(
&self,
method: &str,
params: &P,
) -> Result<Value, LxAppError>
where
P: Serialize + ?Sized,
{
self.call_view_json_with_in(method, params, ViewCallOptions::default())
.await
}
pub async fn call_view_json_with_in<P>(
&self,
method: &str,
params: &P,
options: ViewCallOptions,
) -> Result<Value, LxAppError>
where
P: Serialize + ?Sized,
{
self.call_view_json_value(method, serialize_view_call_params(params)?, options)
.await
}
async fn call_view_json_value(
&self,
method: &str,
params: Option<Value>,
options: ViewCallOptions,
) -> Result<Value, LxAppError> {
let pending = crate::view_call::call_view(self, method, params)?;
crate::view_call::await_pending_view_call(pending, options.timeout()).await
}
}
impl WebViewDelegate for PageInstance {
fn on_page_started(&self) {
self.set_render_status(PageRenderStatus::Started);
}
fn on_page_finished(&self) {
self.handle_loaded();
}
fn handle_post_message(&self, msg: String) {
match IncomingMessage::from_json_str(&msg) {
Ok(incoming) => {
if let Err(e) = self.bridge().handle_incoming(self, Arc::new(incoming)) {
error!("Failed to handle view message: {}", e)
.with_appid(self.inner.appid.clone());
}
}
Err(e) => {
error!("Invalid postMessage JSON: {}", e)
.with_appid(self.inner.appid.clone())
.with_path(self.inner.path.clone());
}
}
}
fn log(&self, level: LogLevel, message: &str) {
let log_level = match level {
LogLevel::Error => LxLogLevel::Error,
LogLevel::Warn => LxLogLevel::Warn,
LogLevel::Info => LxLogLevel::Info,
LogLevel::Debug => LxLogLevel::Debug,
LogLevel::Verbose => LxLogLevel::Debug, };
LogBuilder::new(LogTag::WebViewConsole, message)
.with_level(log_level)
.with_path(&self.inner.path)
.with_appid(self.inner.appid.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, serde::Deserialize)]
struct ViewReply {
ok: bool,
}
#[test]
fn serialize_view_call_params_skips_null() {
assert_eq!(serialize_view_call_params(&()).unwrap(), None);
assert_eq!(
serialize_view_call_params(&serde_json::json!({ "topic": "status" })).unwrap(),
Some(serde_json::json!({ "topic": "status" }))
);
}
#[test]
fn decode_view_call_result_deserializes_typed_payload() {
let reply: ViewReply =
decode_view_call_result("example.echo", serde_json::json!({ "ok": true })).unwrap();
assert!(reply.ok);
}
#[test]
fn decode_view_call_result_reports_method_name() {
let err = decode_view_call_result::<ViewReply>("example.echo", serde_json::json!({}))
.unwrap_err();
match err {
LxAppError::Bridge(message) => {
assert!(message.contains("example.echo"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn view_call_options_default_timeout_is_positive() {
assert!(ViewCallOptions::default().timeout() > Duration::ZERO);
}
}
impl Drop for PageInstanceInner {
fn drop(&mut self) {
if let Ok(mut webview) = self.webview.lock() {
if let Some(_webview_controller) = webview.take() {
info!("WebView destroyed for page")
.with_appid(self.appid.clone())
.with_path(self.path.clone());
}
}
}
}