lingxia-browser 0.10.0

Browser runtime capability crate for LingXia
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
//! WebView lifecycle for browser tabs: creation, the per-tab delegate, the
//! ready/replay flow, and find/load/destroy helpers.

use crate::BUILTIN_BROWSER_APPID;
use crate::chooser::browser_choose_files;
use crate::downloads::browser_download_resource;
use crate::internal_pages::{
    LingxiaSchemeContext, browser_attach_tab_page, browser_resolve_delegate_context,
    browser_resolve_delegate_page, ensure_browser_startup_page, handle_browser_lingxia_scheme,
};
use crate::policy::{
    LINGXIA_SCHEME, extract_url_scheme, handle_browser_navigation_policy,
    normalize_browser_target_url,
};
use crate::tabs::{
    TabCreateState, browser_clear_pending_if_token_matches,
    browser_commit_navigation_if_token_matches, browser_remove_tab_if_token_matches,
    browser_tab_create_state, ensure_browser_lxapp,
};
use crate::types::{BrowserNavigationPolicyDecision, BrowserNavigationPolicyRequest};
use lingxia_log::{LogBuilder, LogLevel as LxLogLevel, LogTag};
use lingxia_platform::traits::app_runtime::{AppRuntime, OpenUrlRequest, OpenUrlTarget};
use lingxia_webview::runtime::{
    destroy_webview as destroy_managed_webview, find_webview as find_managed_webview,
};
use lingxia_webview::{
    LogLevel, NavigationPolicy, NewWindowPolicy, WebTag, WebView, WebViewBuilder,
    WebViewController, WebViewDelegate, WebViewSession,
};
use lxapp::LxAppError;
use serde_json::Value;
use std::sync::Arc;

// ---------------------------------------------------------------------------
// Browser tab WebView delegate
// ---------------------------------------------------------------------------

/// WebView delegate for browser tab WebViews.
///
/// All tab WebViews share a single headless startup PageInstance (and its PageSvc).
/// This delegate routes postMessage, page-started, and page-finished events
/// from the currently active tab WebView to that shared startup PageInstance.
struct BrowserTabDelegate {
    tab_id: String,
    page_path: String,
    session_id: u64,
}

impl WebViewDelegate for BrowserTabDelegate {
    fn on_page_started(&self) {
        match browser_resolve_delegate_page(&self.page_path, self.session_id) {
            Ok(page) => page.notify_page_started(),
            Err(err) => {
                lxapp::warn!(
                    "[InternalBrowser] Failed to resolve delegate page for tab {} on start: {}",
                    self.tab_id,
                    err
                );
            }
        }
    }

    fn on_page_finished(&self) {
        match browser_resolve_delegate_page(&self.page_path, self.session_id) {
            Ok(page) => page.handle_loaded(),
            Err(err) => {
                lxapp::warn!(
                    "[InternalBrowser] Failed to resolve delegate page for tab {} on finish: {}",
                    self.tab_id,
                    err
                );
            }
        }
    }

    fn on_title_changed(&self, title: &str) {
        // Mirror the document title into the tab state (fires the
        // tabs-changed observer for shell sidebars). Platforms whose host
        // layer reports titles separately (e.g. macOS KVO) do not call this.
        let _ = crate::tabs::browser_update_tab_info(&self.tab_id, None, Some(title));
    }

    fn on_favicon_changed(&self, png_bytes: Vec<u8>) {
        // Mirror the page favicon into the tab state (fires the tabs-changed
        // observer for shell sidebars); empty bytes clear a stale favicon.
        let _ = crate::tabs::browser_update_tab_favicon(&self.tab_id, png_bytes);
    }

    fn handle_post_message(&self, msg: String) {
        if let Some((level, message)) = decode_console_envelope(&msg) {
            self.log(level, &message);
            return;
        }

        match browser_resolve_delegate_page(&self.page_path, self.session_id) {
            Ok(page) => {
                if let Err(err) = page.handle_incoming_message_json(&msg) {
                    lxapp::warn!(
                        "[InternalBrowser] Failed to handle bridge message for tab {}: {}",
                        self.tab_id,
                        err
                    );
                }
            }
            Err(err) => {
                lxapp::warn!(
                    "[InternalBrowser] Failed to resolve delegate page for tab {}: {}",
                    self.tab_id,
                    err
                );
            }
        }
    }

    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 | LogLevel::Verbose => LxLogLevel::Debug,
        };
        LogBuilder::new(LogTag::WebViewConsole, message)
            .with_level(log_level)
            .with_path(&self.page_path)
            .with_appid(BUILTIN_BROWSER_APPID.to_string());
    }
}

// ---------------------------------------------------------------------------
// WebView helpers — thin wrappers around lingxia-webview cross-platform API
// ---------------------------------------------------------------------------

fn decode_console_envelope(msg: &str) -> Option<(LogLevel, String)> {
    let json = serde_json::from_str::<Value>(msg).ok()?;
    json.get("__lingxia_console__")
        .and_then(Value::as_bool)
        .filter(|enabled| *enabled)?;
    let level = match json.get("level").and_then(Value::as_str) {
        Some("error") => LogLevel::Error,
        Some("warn") => LogLevel::Warn,
        Some("debug") => LogLevel::Debug,
        Some("info") => LogLevel::Info,
        Some("verbose") => LogLevel::Verbose,
        _ => LogLevel::Info,
    };
    let message = json.get("message").and_then(Value::as_str)?.to_string();
    Some((level, message))
}

fn browser_webtag(path: &str, session_id: u64) -> WebTag {
    WebTag::new(BUILTIN_BROWSER_APPID, path, Some(session_id))
}

pub(crate) fn browser_create_webview(
    path: &str,
    session_id: u64,
    tab_id: &str,
    create_token: u64,
) -> Result<(), LxAppError> {
    let webtag = browser_webtag(path, session_id);
    let browser_owner = ensure_browser_lxapp()?;
    let tab_path_owned = path.to_string();
    let tab_id_owned = tab_id.to_string();

    // Ensure the JS worker and browser startup page exist before creating the tab WebView.
    ensure_browser_startup_page(&browser_owner)?;

    let tab_id_for_lx = tab_id_owned.clone();
    let tab_path_for_lx = tab_path_owned.clone();
    let lingxia_ctx = Arc::new(LingxiaSchemeContext {
        browser: browser_owner.clone(),
        startup_path: browser_owner.initial_route(),
        tab_id: tab_id_owned.clone(),
        tab_path: tab_path_owned.clone(),
        session_id,
    });
    let runtime_for_nav = browser_owner.runtime.clone();
    let owner_appid_for_nav = browser_owner.appid.clone();
    let owner_session_for_nav = browser_owner.session_id();
    let runtime_for_new_window = browser_owner.runtime.clone();
    let owner_appid_for_new_window = browser_owner.appid.clone();
    let owner_session_for_new_window = browser_owner.session_id();
    let tab_id_for_new_window = tab_id_owned.clone();
    let tab_path_for_new_window = tab_path_owned.clone();
    let tab_id_for_download = tab_id_owned.clone();
    let owner_for_download = browser_owner.clone();
    let owner_for_file_chooser = browser_owner.clone();
    let session = WebViewBuilder::browser(webtag)
        .delegate(Arc::new(BrowserTabDelegate {
            tab_id: tab_id_owned.clone(),
            page_path: tab_path_owned.clone(),
            session_id,
        }))
        .on_scheme("lx", move |req| {
            let tab_id = tab_id_for_lx.clone();
            let tab_path = tab_path_for_lx.clone();
            async move {
                match browser_resolve_delegate_context(&tab_path, session_id) {
                    Ok((owner, page)) => owner.handle_lingxia_request(&page, req).into(),
                    Err(err) => {
                        lxapp::warn!(
                            "[InternalBrowser] Failed to resolve lx:// owner for tab {}: {}",
                            tab_id,
                            err
                        );
                        None.into()
                    }
                }
            }
        })
        .on_scheme(LINGXIA_SCHEME, move |req| {
            let ctx = lingxia_ctx.clone();
            async move { handle_browser_lingxia_scheme(&ctx, req).await.into() }
        })
        .on_navigation(move |url| {
            // Keep internal lx:// and lingxia:// browser pages inside this WebView.
            if matches!(extract_url_scheme(url).as_deref(), Some("lx" | "lingxia")) {
                return NavigationPolicy::Allow;
            }
            // This callback only provides the URL string; user-gesture/main-frame
            // metadata is unavailable here, so treat it as a no-gesture navigation.
            // http/https stay in-webview regardless, while external-scheme launches
            // are cancelled in-page: they must come through a platform policy path
            // that carries real gesture data (see handle_browser_navigation_policy).
            let decision = handle_browser_navigation_policy(BrowserNavigationPolicyRequest {
                raw_url: url.to_string(),
                has_user_gesture: false,
                is_main_frame: false,
            });
            match decision.decision {
                BrowserNavigationPolicyDecision::InWebview => NavigationPolicy::Allow,
                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
                }
                BrowserNavigationPolicyDecision::Deny => NavigationPolicy::Cancel,
            }
        })
        .on_new_window(move |url| {
            let normalized = normalize_browser_target_url(url);
            // A standalone (docked aside) browser has no tab strip, so a
            // new-window request (`target=_blank`, `window.open`) has nowhere to
            // surface as a tab. Navigate the aside's own WebView to the target
            // instead — the tabless-browser behavior. The load is deferred onto
            // the executor (which marshals back to the main thread async) so we
            // never call loadRequest re-entrantly inside the createWebView
            // delegate, which aborts the process.
            if crate::tabs::is_standalone_tab(&tab_id_for_new_window) {
                let path = tab_path_for_new_window.clone();
                rong::RongExecutor::global().spawn(async move {
                    let _ = browser_load_url(&path, session_id, &normalized);
                });
                return NewWindowPolicy::Cancel;
            }
            let _ = runtime_for_new_window.open_url(OpenUrlRequest {
                owner_appid: owner_appid_for_new_window.clone(),
                owner_session_id: owner_session_for_new_window,
                url: normalized,
                target: OpenUrlTarget::NewBrowserTab,
            });
            NewWindowPolicy::Cancel
        })
        .on_download(move |request| {
            let tab_id = tab_id_for_download.clone();
            let owner = owner_for_download.clone();
            rong::RongExecutor::global().spawn(async move {
                browser_download_resource(owner, tab_id, request).await;
            });
        })
        .on_file_chooser(move |request| {
            let owner = owner_for_file_chooser.clone();
            async move { browser_choose_files(owner, request).await }
        })
        .create();

    rong::RongExecutor::global().spawn(async move {
        browser_on_webview_ready(
            tab_path_owned,
            session_id,
            tab_id_owned,
            create_token,
            session,
        )
        .await;
    });
    Ok(())
}

async fn browser_on_webview_ready(
    path: String,
    session_id: u64,
    tab_id: String,
    create_token: u64,
    session: WebViewSession,
) {
    let webview = match session.wait_ready().await {
        Ok(webview) => webview,
        Err(e) => {
            lxapp::warn!(
                "[InternalBrowser] Failed to create webview for tab {}: {}",
                tab_id,
                e
            );
            browser_remove_tab_if_token_matches(&tab_id, session_id, create_token);
            return;
        }
    };
    let tab_state = browser_tab_create_state(&tab_id, session_id, create_token);
    match tab_state {
        TabCreateState::Missing => {
            // Tab was closed while creation was in-flight.
            browser_destroy_webview(&path, session_id);
        }
        TabCreateState::Stale => {
            // A newer create lifecycle already took ownership of this tab id.
            // Destroy the orphaned webview from this old create cycle.
            browser_destroy_webview(&path, session_id);
        }
        TabCreateState::Active { pending_url } => {
            if let Some(url) = pending_url {
                // Internal browser pages (`lingxia://X`) need the startup bridge attached
                // so they can communicate with the JS app service worker.
                let is_browser_internal =
                    extract_url_scheme(&url).as_deref() == Some(LINGXIA_SCHEME);
                if is_browser_internal {
                    if let Err(e) = browser_attach_tab_page(
                        webview.clone(),
                        &path,
                        session_id,
                        &tab_id,
                        Some(url.as_str()),
                    )
                    .await
                    {
                        lxapp::warn!(
                            "[InternalBrowser] Failed to attach startup page for internal tab {}: {}",
                            tab_id,
                            e
                        );
                        browser_clear_pending_if_token_matches(&tab_id, session_id, create_token);
                        let _ = webview.load_url("about:blank");
                    } else {
                        browser_commit_navigation_if_token_matches(
                            &tab_id,
                            session_id,
                            create_token,
                            Some(&url),
                        );
                    }
                } else {
                    // Direct URL load — no bridge handshake needed, just navigate.
                    if let Err(e) = webview.load_url(&url) {
                        lxapp::warn!(
                            "[InternalBrowser] Failed to load URL for tab {}: {}",
                            tab_id,
                            e
                        );
                        browser_clear_pending_if_token_matches(&tab_id, session_id, create_token);
                    } else {
                        browser_commit_navigation_if_token_matches(
                            &tab_id,
                            session_id,
                            create_token,
                            Some(&url),
                        );
                    }
                }
            } else {
                // Startup page: attach WebView to shared startup PageInstance, then load with nonce.
                if let Err(e) =
                    browser_attach_tab_page(webview.clone(), &path, session_id, &tab_id, None).await
                {
                    lxapp::warn!(
                        "[InternalBrowser] Failed to load startup page for tab {}: {}",
                        tab_id,
                        e
                    );
                    let _ = webview.load_url("about:blank");
                } else {
                    browser_commit_navigation_if_token_matches(
                        &tab_id,
                        session_id,
                        create_token,
                        None,
                    );
                }
            }
        }
    }
}

pub(crate) fn browser_find_webview(
    path: &str,
    session_id: u64,
) -> Result<Arc<WebView>, LxAppError> {
    let webtag = browser_webtag(path, session_id);
    find_managed_webview(&webtag).ok_or_else(|| {
        LxAppError::ResourceNotFound(format!("browser webview not found: {}", webtag.as_str()))
    })
}

pub(crate) fn browser_load_url(path: &str, session_id: u64, url: &str) -> Result<(), LxAppError> {
    let webview = browser_find_webview(path, session_id)?;
    webview
        .load_url(url)
        .map_err(|e| LxAppError::WebView(e.to_string()))
}

pub(crate) fn browser_destroy_webview(path: &str, session_id: u64) {
    let webtag = browser_webtag(path, session_id);
    // Remove from global registry (triggers platform-specific cleanup on Drop).
    destroy_managed_webview(&webtag);
}