# Webview

A page is a `WebView` handle that your app owns. The `webview(id, &page)` element only shows it.
That split is the whole design: switching tabs or moving the element to another window never
reloads the page, because the element is not what holds it.

> **Example: Browser.** Source in [examples/browser](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser). Live on the web and captured on macOS, Linux and Windows in the docs app.


On macOS this is shipped with WKWebView and, as an opt-in, Chromium through CEF. On Linux and
Windows the engines are not written yet, see [what is missing](#linux-and-windows).

## The handle and the element

Create a page, keep it in your state, and mount it:

```rust
fn options() -> WebViewOptions {
    WebViewOptions::default().adopt_popups().inspectable(true)
}
```

<sub>From [examples/browser/src/lib.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser/src/lib.rs), line 46.</sub>


`WebView::with_options(options())` makes the handle and `page.load(url)` starts it. `WebView::new()`
and `WebView::with_url(url)` are the short forms. The options are fixed at creation:

| Option | Effect |
| --- | --- |
| `.profile(..)` | `Default`, `Private` (memory only, gone with the last page) or `Named(name)` (its own persistent store) |
| `.engine(..)` | `System` or `Cef` for this page |
| `.adopt_popups()` | `window.open` hands you a live page that keeps `window.opener`, not only a URL |
| `.inspectable(..)` | allows Web Inspector or DevTools, on by default in debug builds |
| `.user_agent(..)` | the user agent string |

The element goes in your render, keyed by an id of your choosing, with a handler for each event
you care about:

```rust
webview(SharedString::from(format!("page-{id}")), &tab.page)
    .size_full()
    .context_menu_items(vec![
        MenuItem::new("open-link-tab", "Open Link in New Tab").contexts(Contexts::LINK),
        MenuItem::new("copy-address", "Copy Page Address"),
    ])
    .on_navigate(cx.listener(move |this, e: &Navigate, _, cx| {
        if this.selected == id {
            this.address = e.url.clone().into();
        }
        update(cx, this, &|t| t.url = e.url.clone().into());
    }))
    .on_title_changed(cx.listener(move |this, e: &TitleChanged, _, cx| {
        let title = if e.title.is_empty() { "Untitled".to_owned() } else { e.title.clone() };
        update(cx, this, &|t| t.title = title.clone().into());
    }))
```

<sub>From [examples/browser/src/lib.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser/src/lib.rs), line 113.</sub>


A page can be mounted by one element at a time, the last one. Unmounting parks it: it keeps its
state and keeps loading. It closes when the last clone of the handle drops. Clones share the
page and compare with `==`.

## Commands

Commands are methods on the handle, and they are safe to call before the page has a view. They
queue and replay in order once it does. The browser example maps its toolbar actions onto them:

```rust
.on_action(cx.listener(|this, _: &Reload, _, _| {
    if let Some(page) = this.current() {
        if page.is_loading() { page.stop() } else { page.reload() }
    }
}))
.on_action(cx.listener(|this, _: &Back, _, _| {
    if let Some(page) = this.current() {
        page.go_back();
    }
}))
.on_action(cx.listener(|this, _: &Forward, _, _| {
    if let Some(page) = this.current() {
        page.go_forward();
    }
}))
.on_action(cx.listener(|this, _: &DevTools, _, _| {
    if let Some(page) = this.current() {
        page.open_dev_tools();
    }
}))
```

<sub>From [examples/browser/src/lib.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser/src/lib.rs), line 233.</sub>


The set: `load`, `load_html`, `go_back`, `go_forward`, `reload`, `stop`, `set_zoom`,
`set_user_agent`, `open_dev_tools`, `find` with `find_next`, `find_previous` and `stop_find`,
`set_muted`, `focus`, `request_close`, `start_download`, `save_session` and `restore_session`,
the cookie calls, and the user script and script message calls below. State reads are
immediate: `url()`, `title()`, `is_loading()`, `can_go_back()`, `can_go_forward()`.

Anything that has an answer takes a callback instead of returning. nd has no executor, so the
answer arrives as an `FnOnce(T, &mut App)`, as with the dialogs:

```rust
page.execute_javascript("document.title", |title, cx| { /* Result<JsValue, JsError> */ });
```

A thrown exception is an `Err` with its message and a returned promise is awaited.
`get_cookies` and `save_session` answer the same way. `execute_javascript_in` runs in a named
isolated world.

## Events

Handlers are `on_navigate`, `on_title_changed`, `on_loading_changed`, `on_load_progress`,
`on_load_failed`, `on_new_window`, `on_window_closed`, `on_download_requested`,
`on_download_updated`, `on_context_menu_item_clicked`, `on_script_message` and the rest of
those in [events.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/crates/nd-webview/src/events.rs). Each delivers on the UI thread,
never inside an engine callback.

### Popups and tabs

With `adopt_popups`, `NewWindow` carries `popup: Some(page)`: a page the engine already
started, with `window.opener` intact. Add it as a tab and mount it. Without a popup you get the
URL and the `Disposition`, such as `BackgroundTab`, and open a page yourself.

```rust
.on_new_window(cx.listener(move |this, e: &NewWindow, _, cx| {
    let select = e.disposition != nd::webview::Disposition::BackgroundTab;
    match &e.popup {
        Some(popup) => {
            this.push(popup.clone(), select);
        }
        None => this.open_tab(&e.url, select),
    }
    cx.notify();
}))
.on_window_closed(cx.listener(move |this, _: &WindowClosed, _, cx| {
    this.close(id);
    cx.notify();
}))
```

<sub>From [examples/browser/src/lib.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser/src/lib.rs), line 141.</sub>


### Downloads

A response the engine does not render, or an attachment, arrives as a `Download` that waits for
you. Choose where it goes with `save_to(path)` or `save_to_downloads()`, or `cancel()` it. `on_download_updated` then reports
`Running`, `Done`, `Failed` or `Cancelled` with the byte counts.

```rust
.on_download_requested(cx.listener(|this, e: &DownloadRequested, _, cx| {
    let path = e.download.save_to_downloads();
    this.status = format!("Downloading {}", path.display()).into();
    cx.notify();
}))
.on_download_updated(cx.listener(|this, e: &DownloadUpdated, _, cx| {
    this.status = match e.state {
        DownloadState::Running => format!("Downloaded {} KB", e.received / 1024),
        DownloadState::Done => {
            format!("Saved {}", e.path.as_ref().map(|p| p.display().to_string()).unwrap_or_default())
        }
        DownloadState::Failed => format!("Download failed: {}", e.error.clone().unwrap_or_default()),
        DownloadState::Cancelled => "Download cancelled".into(),
    }
    .into();
    cx.notify();
}))
```

<sub>From [examples/browser/src/lib.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser/src/lib.rs), line 157.</sub>


## Requests the app can leave unanswered

Permission prompts (camera, microphone, geolocation, notifications, clipboard), custom scheme
requests and downloads each wait for an answer through a responder on the event. If the app
drops the event without answering, the engine falls back instead of leaving the page waiting:
WebKit shows its own permission prompt, a scheme request fails, a download is cancelled. A page
never hangs on an app that registered no handler.

To answer a permission request, handle `on_permission_request` and call `respond(decision)` on
its responder. A custom scheme is registered with `nd::webview::register_scheme("app")` before the
first page is created and served through `on_scheme_request`.

## Scripts and messages

`add_user_script(UserScript::new(id, source))` injects a script into every page load, optionally
in an isolated world. `register_script_message("name", None)` lets the page call
`window.webkit.messageHandlers.name.postMessage(value)` on both engines, and
`on_script_message` receives the `ScriptMessage { name, body, .. }` as JSON.

## Context menus

By default the page shows the engine's own context menu. `context_menu_items(vec![..])` merges
your items into it in place, filtered by `Contexts` (link, image and so on), and a chosen one
arrives as `ContextMenuItemClicked` with the page and link URLs. `context_menu_mode` can
suppress the engine's menu or leave it native. The browser example adds "Open Link in New Tab"
this way.

## Engines

| | System | CEF |
| --- | --- | --- |
| macOS | WKWebView, an `NSView` beside the window's other views | Chromium rendering off-screen, composited into the window |
| Windows | not written yet | not packaged yet |
| Linux | not written yet | not packaged yet |
| Web | not available | not available |

The default engine is `[webview] engine` in `nativedesktop.toml`: `"system"` or `"cef"`. The
choice is baked at build time. Override it per page with `WebViewOptions::engine`, and for a
whole build and run with `ND_WEBVIEW_ENGINE=cef`, which `nd package` also honours. `engine =
"cef"` needs nd's `cef` feature; without it the app carries no Chromium bytes, and packaging
bundles Chromium only when the setting asks for it. The browser example sets `engine = "system"`
in its [nativedesktop.toml](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/browser/nativedesktop.toml) and builds with CEF
through the environment variable.

`[webview.cef] style = "chrome"`, a windowed Chrome-style mode for extensions, is accepted by the
config but not implemented. Only the off-screen style runs.

WKWebView's inspector opens through the private `_inspector` SPI, as wry does. Without it the
page stays inspectable from Safari.

## Overlays over a page

A page is a native view, so a drawn element would normally sit under it. Popovers, menus and
panels are above web views and never pass input through to them. See
[overlays](https://nativedesktop.formalsnake.dev/guides/overlays.md#input-never-leaks). `native_surface(..).count_input()` in the overlays
example counts what a page received, which is how the leak gate proves it.

## Linux and Windows

On both, `webview()` renders a notice with the reason, and every command fails with it. The
planned engines are WebView2 in composition mode on Windows and WPE WebKit on Linux, with CEF as
the fallback if WPE does not embed. Nothing of the engine matrix runs there. Write the app
against the handle as on macOS and it works once the engines land; until then, test the web
parts on macOS.
