day-piece-webview 0.4.4

An embedded native web view for Day apps: remote pages and bundled sites
Documentation
---
title: "Web view integration"
description: "Load web content in a Day app, handle navigation, and manage view lifetime."
---
<!-- Copyright © The Daybrite Project; SPDX-License-Identifier: CC-BY-SA-4.0 -->
# Web view integration

Use `web_view` for URLs, `web_view_inline` for bundled sites, and
[`web_view_resources`](resource-provider.md) for app-provided bytes. All return a `WebView`
that implements Day's `Piece` and accepts layout, accessibility, and ID decorators.

The examples below belong in a Day app with `day::resources!()`. Names such as
`res::str::go()` and `res::assets::site` stand for the app's generated translations and asset
directories. Define the strings in each supported locale. Do not replace them with hardcoded
UI text or string-based resource lookups.

## A browser with navigation controls

```rust
use day::prelude::*;
use day_piece_webview::{support, web_view};

fn browser() -> impl Piece {
    let url = Signal::new("https://daybrite.dev".to_owned());
    let go = Trigger::new();
    let back = Trigger::new();
    let reload = Trigger::new();
    let history = support() == Support::Native;
    column((
        row((
            button(res::str::back())
                .enabled(move || history)
                .action(move || back.notify()),
            text_field(url).id("browser-url"),
            button(res::str::go()).action(move || go.notify()),
            button(res::str::reload()).action(move || reload.notify()),
        )).spacing(8.0),
        web_view(url).go(go).back(back).reload(reload).id("browser"),
    ))
}
```

The initial URL loads when the view is created. Editing the signal alone does not navigate;
notify the `go` trigger to load its current value. Native navigation updates the signal with
the reported URL. web-dom does not report iframe URL changes.

| Modifier | On trigger notification |
|---|---|
| `.go(trigger)` | Load the current URL signal |
| `.back(trigger)` / `.forward(trigger)` | Move through browser history |
| `.stop(trigger)` | Stop the current load |
| `.reload(trigger)` | Reload; web-dom instead reloads the last URL supplied by the app |
| `.force_reload(trigger)` | Bypass the resource cache; check `force_reload_support()` first |

Bind a `JsHandle` with `.js(handle)` and await `handle.navigation_state()` to read a native
`NavigationState` containing `url`, `title`, `can_go_back`, `can_go_forward` and `loading`.
This reads engine state without running page JavaScript or making network requests. Use its
history flags to enable Back/Forward and its loading flag for Stop and a loading indicator.
The demo polls while mounted, with a bounded timeout and a mount-generation check to discard
stale results; disposing the root aborts polling. Iframe hosts return `EvalError::Unsupported` because cross-origin history and
page state are unavailable.

For inline or resource-provider views, `.url_binding(address_signal)` exposes the reported URL
to an address bar while preserving the constructor's initial page when the signal is empty.
Editing that signal alone still does not navigate; notify `.go(trigger)` explicitly.

The web view grows in both directions. A `column` containing controls followed by the web view
lets it use the remaining height. Give an embedded preview a bounded frame; avoid putting an
unbounded web view inside a scrolling container. Native child-window hosts need particular care
with clipping and overlapping controls; see [Platforms](platforms.md).

## Bundled help, editors, and players

Place the complete web document tree under a generated asset directory:

```text
resource/assets/site/
  index.html
  css/style.css
  js/main.js
  pages/shortcuts.html
```

Open it with its generated constant:

```rust
use day::prelude::*;
use day_piece_webview::web_view_inline;

fn shortcuts() -> impl Piece {
    web_view_inline(res::assets::site)
        .start_page("pages/shortcuts.html?mode=compact#keyboard")
        .id("shortcuts")
}
```

`.start_page()` applies only to inline sites. Without it the first page is `index.html`.
The query and fragment reach the page unchanged. Within HTML and CSS, use relative links such
as `../css/style.css`; do not construct platform-specific bundle URLs.

For an explicit preparation step:

```rust
use day_piece_webview::{AssetDirSiteExt, PrepareError, WebView, web_view_inline};

async fn prepared_help() -> Result<WebView, PrepareError> {
    let site = res::assets::site.prepare_site().await?;
    Ok(web_view_inline(site))
}
```

`prepare_site()` checks for `index.html` on native targets and prepares Linux GTK's extracted
cache. Its current implementation performs the work synchronously and returns an immediately
ready future; `await` does not move extraction off the UI thread. web-dom skips the presence
check, so missing pages fail when the iframe loads. Map preparation errors to localized UI
messages; the error's `Display` text is a diagnostic, not a translated label.

### Share other bundled assets

A player can opt into reading sibling asset directories:

```rust
use day::prelude::*;
use day_piece_webview::web_view_inline;

fn player() -> impl Piece {
    web_view_inline(res::assets::player).app_assets().transparent()
}
```

`.app_assets()` widens Apple's file read-access root and GTK's extraction scope. Other engines
already expose the staged asset tree. It is not a portable filesystem sandbox. File-origin
`fetch` and XMLHttpRequest rules also vary by engine. Use a
[resource provider with a bundled shell](resource-provider.md#bundled-shell-and-dynamic-content)
when the page needs app-owned data at relative URLs.

`.transparent()` clears the native view background. The document must also leave its own
background transparent, for example `html, body { background: transparent; }`. An opaque CSS
background remains opaque. Test compositing on the intended platforms; the walkthrough does
not currently assert transparency.

## Links from web content into the app

Inline and resource views keep internal navigation in the web view. By default, links that
leave their content root open through the operating system. Override that policy for routes
owned by the application:

```rust
use day::prelude::*;
use day_piece_webview::{LinkPolicy, web_view_inline};

fn help_with_settings(open_settings: impl Fn() + 'static) -> impl Piece {
    web_view_inline(res::assets::site).on_external_link(move |url| {
        if url == "myapp://settings" {
            open_settings();
            LinkPolicy::Ignore
        } else if url.starts_with("https://") || url.starts_with("mailto:") {
            LinkPolicy::OpenSystem
        } else {
            LinkPolicy::Ignore
        }
    })
}
```

The bundled page can link to `myapp://settings`. This is an in-view message convention, not
OS-level scheme registration. The callback runs on the UI thread. Match known routes and validate
parameters before acting on them.

| Policy | Effect |
|---|---|
| `OpenSystem` | Open using the OS URL handler, usually a browser or mail client |
| `Ignore` | Cancel navigation; the callback can handle the request itself |
| `InView` | Request navigation inside the web view |

Ordinary `web_view(url)` documents opt into this callback on AppKit/UIKit when a handler is
provided. User-activated links and new-window requests (`target="_blank"`) are cancelled
before the UI-thread callback runs. Initial loads, `.go()` reloads, embedded frames, and
same-document fragments remain in the web view. `LinkPolicy::InView` deliberately reissues
the request without re-triggering a click callback. Session reuse updates the opt-in.
Other backends retain their ordinary-document navigation behavior; use inline/resource
mode for portable site-boundary policies.

## Retain a page across app navigation

```rust
use day::prelude::*;
use day_piece_webview::{WebSession, web_view_inline};

fn retained_editor() -> impl Piece {
    web_view_inline(res::assets::editor)
        .session(WebSession::global("app.editor"))
}
```

A normal view is recreated when its Day subtree is rebuilt. Supported backends retain a
session's browser engine, including page state, scroll position, JavaScript context, and history.
Use one stable key for a persistent surface. Attach it to only one visible view at a time.
Hoist any associated Day signals separately; retaining the browser does not retain page-local
Rust state.

Retention is implemented for AppKit, UIKit, macOS GTK, and Qt WebEngine. Linux GTK, Android,
Windows XAML, Wry hosts, Harmony, and web-dom currently ignore the engine-retention request.
There is no disposal API: a session lasts for the process lifetime. Do not allocate one per
book, list item, or transient preview. Resource sessions also pin their first provider and
initial URL; passing a different provider under the same key does not replace that content.

## Choose the right data channel

Use [JavaScript evaluation](webview-eval.md) for small commands and structured state, such as a
chart dataset, selected item, or reader position. Use [resource providers](resource-provider.md)
for documents and binary resources. For an EPUB, serve one requested chapter or image at a time;
do not serialize the entire book into a JavaScript call.

The public API does not currently provide a general load-completion event, download manager,
cookie-store API, arbitrary request-header injection, or a general JavaScript message bridge.
Build a readiness convention for bundled pages and handle evaluation errors during navigation.
[Platform notes](platforms.md) describe the remaining differences.

`on_load(callback)` runs on the UI thread after a native backend reports completed navigation,
including a same-URL reload. Pair it with `JsHandle` to apply the latest UI state after loading;
use a reactive watcher for subsequent changes to avoid reloading and losing reading position.
Emulated iframe hosts do not report navigation and therefore do not invoke this callback.

## Shared logins and private browsing

Use `.profile(WebProfile::persistent(name))` on independent views to share browser storage, or
`WebProfile::private(name)` for an isolated ephemeral store. A session retains a loaded page;
a profile shares its data without retaining a page. See [profiles and privacy](profiles.md)
for native cookie access, complete data deletion, capability checks and platform limits.

## Force reload

Force reload keeps cookies, logins and site storage intact. Apple uses WKWebView's
`reloadFromOrigin`, GTK uses `reload_bypass_cache`, Qt uses `ReloadAndBypassCache`, and
WebView2 uses `Page.reload` with `ignoreCache`. Android temporarily applies `LOAD_NO_CACHE`
and restores its previous mode after completion or Stop. ArkWeb must evict the application's
shared resource cache before refreshing; other WebViews may consequently refetch resources.
The iframe backend reports Unsupported because it cannot guarantee cache bypass.

## Document tabs

The demo uses Day's `TabSet` and `document_tabs`. Each document owns its browser,
address editor, navigation history, profile choice and console state. Switching
or moving a tab retains its native webview; closing it disposes the page scope
and resolves outstanding evaluations with `ViewGone`. Private tabs pass their
profile choice to newly opened tabs and are excluded from Reopen Closed Tab.

`WebView::on_new_tab(foreground_label, background_label, callback)` installs an
application callback receiving `NewTabRequest { url, background }`. Both menu
labels take what `label` takes (a string, a `LocalizedText` from the application's
generated accessors, a signal or a closure) and are resolved when the piece is
built. This event
is separate from `on_external_link`: a new-tab request must not replace the
originating document. The app owns creating and selecting the new tab.

The adapters use engine new-window/navigation events and native link menus where
available. DOM handles modifier clicks and link menus only in same-origin frames;
browser origin restrictions prevent intercepting remote iframe content. A URL
request does not transfer a JavaScript opener, POST body, or live popup browsing
context. Native macOS groups support the system tab tear-off and merge affordances. Other
backends currently reorder within a collection; pinned tabs and persisted session
restoration remain application policy. `dayscript/tabs.yaml` tests the
demo's native-page retention and independent state.

The persisted **Native tabs** preference changes presentation live. On macOS, AppKit,
Qt and GTK use real `NSWindowTabGroup` document windows; the existing webview moves
between its document window and the emulated host without reloading. Elsewhere,
Qt, libadwaita and WinUI use their document-tab controls, and Android uses Material
TabLayout. UIKit deliberately uses the emulated document strip: `UITabBarController`
represents application sections, and UIKit exposes no public Safari browser-tab
container. The ArkUI NDK and DOM backends also use the emulated strip.

Apps can supply `DocumentTabs::chrome` and `layout` to control the entire emulated
strip and its placement. `TabActions` supplies shared add/close commands and scoped
drag/drop reordering. The preference changes chrome and window placement; it does
not recreate document scopes, browser history, page JavaScript, or undo state.

Settings uses Day's singleton preferences-window API with a compact independent
window on desktop. Mobile platforms use their separate-window mechanism when
available; iPhone falls back to a page sheet.

## Optional Servo engine

The separate `day-piece-webview-servo` package adds a per-view engine choice on macos-appkit.
Its `servo` feature is disabled by default; native consumers do not depend on this package.
Native WebView remains available alongside it. Existing platform capability tables
on this page describe native integrations. See [Servo](servo.md) for engine selection,
coexistence, build requirements, profile restrictions, and current host limitations.