Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Documentazione

Web view e video

La web view e il video player incorporano il motore web e il media player della piattaforma come normali view: web_view e video in Rust (nsh_core::prelude), WebView e Video da nsh in TSX. Poiché NSH esegue il rendering con view native, si inseriscono nel layout come qualsiasi altro elemento: scorrono con la view genitore, accettano angoli arrotondati e clipping, e altre view possono sovrapporsi a esse senza problemi di stratificazione.

ComponenteiOSAndroidmacOSLinuxWindows
WebViewWKWebViewandroid.webkit.WebViewWKWebViewWebKitWebView di WebKitGTK 6.0WebView2, in una finestra figlia
VideoAVPlayerLayer; AVPlayerViewController con controlsMediaPlayer su una TextureView; MediaController con controlsAVPlayerLayer; AVPlayerView di AVKit con controlsmedia stream di GTK (GStreamer) in un GtkPicture; GtkVideo con controlsmedia engine di Media Foundation, con i frame disegnati tramite Direct2D; la control bar di NSH con controls

I framework alla base (WebKit, AVFoundation) vengono caricati la prima volta che un’app ne usa uno, quindi le app che non mostrano mai una pagina web o un video non ne pagano il costo all’avvio.

Su macOS la web view non ha una scroll view propria: con scrollable={false} la rotellina del mouse e il trackpad fanno scorrere le scroll view dell’app attorno alla pagina invece della pagina stessa, che continua a scorrere con la tastiera e con i propri script.

Su Linux una web view è una pagina WebKitGTK disegnata all’interno della propria view: i contenuti NSH sopra di essa (un dialog, una sheet) la coprono, e le view genitore la ritagliano. La capability webview si applica anche alle navigazioni dei frame (un iframe di un host non consentito resta vuoto), e scrollable: false impedisce alla pagina stessa di scorrere (gli elementi al suo interno continuano a scorrere). Le app richiedono WebKitGTK 6.0 installato; un runtime compilato senza di esso mostra un placeholder neutro. I video riproducono i formati che i plugin GStreamer installati sanno decodificare; rate non è supportato (vengono riprodotti a 1×), e onEnd è disponibile solo per i video che non sono in loop.

Su Windows una web view è un controllo WebView2 in una finestra figlia che segue la propria view: viene ritagliata dalle view genitore e dagli angoli arrotondati, ma i contenuti NSH disegnati sopra di essa (un dialog, una sheet) restano sotto. Richiede il WebView2 Runtime (incluso in Windows 11; installato con Edge su Windows 10), altrimenti mostra un placeholder; baseUrl per HTML, scrollable e allowsBackForwardNavigationGestures non sono supportati. I video sono contenuti ordinari (ritagliati, trasformati e coperti come qualsiasi view) e riproducono i formati che i codec di Media Foundation installati sanno decodificare; rate funziona, non c’è il poster, e la control bar è sempre visibile finché controls è attivo.

#WebView

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    web_view("https://en.m.wikipedia.org/wiki/Swift_(programming_language)").flex_1()
}

#Sorgenti

Una web view mostra una tra src e html:

PropsMostra
src="https://…"Una pagina web.
html="<!doctype html>…"HTML da una stringa, mostrato al posto di src.
html con baseUrl="https://example.com/"HTML i cui URL relativi vengono risolti rispetto a baseUrl.

In Rust, web_view(url) mostra una pagina e web_view_html(html, base_url) mostra HTML (base_url un Option<&str>); l’URL e l’HTML possono essere un signal o una closure, che ricarica la pagina quando ciò che legge cambia. Cambiare src o html carica la nuova pagina.

#Props

In Rust ogni prop è un metodo builder in snake case (injected_java_script, java_script_enabled, scrollable, user_agent; injected_java_script_before_content per injectedJavaScriptBeforeContentLoaded, back_forward_gestures per allowsBackForwardNavigationGestures di iOS) e ogni evento un metodo on_… (on_navigation_state_change) il cui listener riceve un WebNavigation (url, title, loading, can_go_back, can_go_forward) oppure, per on_error e on_message, un &str.

PropTipoDefaultDescrizione
srcstringL’URL della pagina da mostrare.
htmlstringHTML inline, mostrato al posto di src.
baseUrlstringRispetto a cosa vengono risolti gli URL relativi di html.
injectedJavaScriptstringViene eseguito dopo il caricamento di ogni pagina.
injectedJavaScriptBeforeContentLoadedstringViene eseguito prima degli script di ogni pagina.
javaScriptEnabledbooleantrueLe pagine eseguono script.
scrollablebooleantrueL’utente può far scorrere la pagina.
userAgentstringLa stringa user agent del browser.
ios={{ allowsBackForwardNavigationGestures }}booleaniOS: scorri per andare indietro e avanti nella cronologia della pagina.
desktop={{ allowsBackForwardNavigationGestures }}booleanmacOS: scorri con due dita sul trackpad per andare indietro e avanti; Windows: scorri su un touchscreen; Linux: scorri sul touchpad.

Eventi:

EventoPayloadQuando
onLoadStartWebViewNavigationUna pagina ha iniziato a caricarsi.
onLoadWebViewNavigationUna pagina ha finito di caricarsi.
onError{ message }Il caricamento non è riuscito.
onNavigationStateChangeWebViewNavigationL’URL, il titolo, lo stato di caricamento o la cronologia sono cambiati.
onMessage{ data: string }La pagina ha inviato un messaggio (sotto).

WebViewNavigation è { url, title, loading, canGoBack, canGoForward }.

#Un browser con indietro e avanti

Il ref della web view (un element_ref() applicato con .element_ref(&web) in Rust, ref in TSX) ha reload, go_back / goBack, go_forward / goForward, stop_loading / stopLoading, inject_java_script / injectJavaScript(script), post_message / postMessage(data) e send_input / sendInput(input) (sotto):

use nsh_core::prelude::*;

pub fn browser() -> impl IntoElement {
    let web = element_ref();
    let nav = signal(None::<WebNavigation>);
    let button = move |label: &'static str, enabled: fn(&WebNavigation) -> bool, action: fn(&ElementRef)| {
        let enabled = move || nav.with(|n| n.as_ref().is_some_and(enabled));
        text(label)
            .p(px(12.0))
            .text_color(move || if enabled() { rgb(0x0969da) } else { rgb(0x8c959f) })
            .disabled(move || !enabled())
            .on_press(move || action(&web))
    };
    view()
        .flex_1()
        .pt(px(50.0))
        .child(
            text(move || {
                nav.with(|n| match n {
                    Some(n) if n.loading => "Loading…".to_owned(),
                    Some(n) => n.title.clone(),
                    None => String::new(),
                })
            })
            .number_of_lines(1)
            .px(px(12.0))
            .text_color(rgb(0x57606a)),
        )
        .child(
            web_view("https://developer.mozilla.org/")
                .element_ref(&web)
                .flex_1()
                .on_navigation_state_change(move |n: &WebNavigation| nav.set(Some(n.clone())))
                .back_forward_gestures(true),
        )
        .child(
            view()
                .flex_row()
                .justify_around()
                .child(button("Back", |n| n.can_go_back, |web| web.go_back()))
                .child(button("Forward", |n| n.can_go_forward, |web| web.go_forward()))
                .child(button("Reload", |_| true, |web| web.reload())),
        )
}

#Messaggi tra la pagina e l’app

Le pagine comunicano con l’app tramite window.nsh.postMessage(string), che arriva come on_message (onMessage). L’app comunica con la pagina tramite post_message del ref (postMessage(string)), che arriva nella pagina come evento message su window. I messaggi sono stringhe; invia JSON per i dati strutturati.

use nsh_core::prelude::*;

const PAGE: &str = r#"<!doctype html>
<html><head><meta name="viewport" content="width=device-width"></head>
<body style="font-family: -apple-system, sans-serif; margin: 16px">
  <button onclick="window.nsh.postMessage(JSON.stringify({ type: 'hello', at: Date.now() }))">
    Post to the app
  </button>
  <p id="got">Nothing from the app yet.</p>
  <script>
    window.addEventListener("message", (e) => {
      document.getElementById("got").textContent = "The app says: " + e.data;
    });
  </script>
</body></html>"#;

#[component]
pub fn App() -> impl IntoElement {
    let web = element_ref();
    let last = signal(String::new());
    view()
        .flex_1()
        .pt(px(50.0))
        .gap(px(12.0))
        .child(
            web_view_html(PAGE, None)
                .element_ref(&web)
                .h(px(240.0))
                .on_message(move |data: &str| {
                    let message: serde_json::Value = serde_json::from_str(data).unwrap_or_default();
                    last.set(message["type"].as_str().unwrap_or_default().to_owned());
                }),
        )
        .child(
            text("Post to the page")
                .p(px(16.0))
                .text_color(rgb(0x0969da))
                .on_press(move || web.post_message("hi from nsh")),
        )
        .child(text(move || format!("Last message: {}", last.get())).px(px(16.0)))
}

inject_java_script del ref (injectJavaScript(script)) esegue codice nella pagina corrente in qualsiasi momento; injected_java_script e injected_java_script_before_content (injectedJavaScript, injectedJavaScriptBeforeContentLoaded) vengono eseguiti a ogni caricamento di pagina, prima o dopo gli script della pagina stessa, ed è il posto giusto per installare gli hook:

web_view("https://example.com")
    .injected_java_script_before_content("window.isNativeApp = true;")
    .injected_java_script("window.nsh.postMessage(document.title);")
    .on_message(|title: &str| log::info!("title {title}"))

#Input come se fosse dell’utente

injectJavaScript può creare solo eventi sintetici, che una pagina vede come non attendibili (event.isTrusted è false) e che alcuni widget ignorano. send_input del ref (sendInput(input)) fornisce alla pagina un input come se provenisse dalle mani dell’utente, quindi i suoi eventi sono attendibili:

  • { type: "click", x, y } clicca un punto del viewport, in pixel CSS (quelli riportati da getBoundingClientRect()).
  • { type: "text", text } digita il testo in ciò che ha il focus della pagina, un tasto alla volta: ogni carattere sul tasto corrispondente del layout US, un a capo come Enter, e ciò che nessun tasto US digita (é, CJK, emoji) su nessun tasto (code è "Unidentified").
  • { type: "key", key } preme Enter, Tab, Escape o Backspace.
use nsh_core::{WebInput, WebKey, prelude::*};
use std::time::Duration;

fn search(web: &ElementRef) {
    web.send_input(WebInput::Click { x: px(120.0), y: px(40.0) });
    let web = *web;
    set_timeout(Duration::from_millis(50), move || {
        web.send_input(WebInput::Text("espresso".into()));
        web.send_input(WebInput::Key(WebKey::Enter));
    });
}

In Rust sono WebInput::Click { x, y }, WebInput::Text(text) e WebInput::Key(WebKey::Enter).

L’input attendibile porta con sé l’attivazione utente (può aprire finestre, scrivere negli appunti, aprire il pannello file del sistema o premere il pulsante di conferma di una pagina), quindi è una capability a sé: l’app ha bisogno di "webview": { "input": true } in capabilities di nsh.json (webview), anche quando non ne dichiara nessuna, altrimenti il core scarta la chiamata.

Come un click, sendInput sposta il focus della tastiera sulla web view, togliendolo al Input dell’app. La pagina gestisce l’input con i propri tempi: gli eventi di un click possono essere ancora in arrivo quando inizia la chiamata successiva, quindi lascia che un click si assesti (circa un frame) prima di digitare dopo di esso. macOS consegna l’input; le altre piattaforme per ora lo ignorano.

Prima di offrire qualcosa che ne dipende, verifica se funziona sulla piattaforma corrente: web_input_available() (WebView.inputAvailable()) risolve true solo dove la piattaforma consegna l’input e l’app possiede webview.input, quindi è false sulle piattaforme che ignorano la chiamata e nelle app senza il grant.

use nsh_core::prelude::*;

let input = signal(false);
spawn(async move { input.set(web_input_available().await) });

#Quali siti può mostrare una web view

Se il tuo nsh.json dichiara "capabilities", una web view può mostrare e navigare solo verso gli host elencati in webview:

{
  "capabilities": {
    "webview": { "hosts": ["example.com", "*.example.com"] }
  }
}

Il controllo copre src della web view e ogni navigazione di primo livello compiuta dalla pagina (link, redirect, modifiche di location); viene deciso in modo nativo quando la piattaforma chiede se una navigazione può procedere. Le sottorisorse della pagina (script, immagini, frame) sono affare del contenuto web e non vengono controllate. *.example.com corrisponde solo ai sottodomini; elenca anche example.com per il dominio senza sottodominio. Un’app senza la chiave "capabilities" può mostrare qualsiasi pagina. Vedi Sandbox e capability.

#Video

use nsh_core::prelude::*;

pub fn player() -> impl IntoElement {
    let player = element_ref();
    let paused = signal(true);
    let time = signal(String::new());
    let link = |label: &'static str| text(label).text_color(rgb(0x0969da));
    view()
        .flex_1()
        .justify_center()
        .p(px(16.0))
        .gap(px(12.0))
        .child(
            video("https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4")
                .element_ref(&player)
                .paused(paused)
                .repeat(true)
                .object_fit(ResizeMode::Contain)
                .h(px(220.0))
                .rounded(px(12.0))
                .overflow_hidden()
                .bg(rgb(0x000000))
                .on_progress(move |e: &VideoProgress| {
                    time.set(format!("{:.1} / {:.1} s", e.current_time, e.duration))
                })
                .on_end(move || paused.set(true)),
        )
        .child(
            view()
                .flex_row()
                .gap(px(16.0))
                .items_center()
                .child(
                    text(move || if paused.get() { "Play" } else { "Pause" })
                        .text_color(rgb(0x0969da))
                        .font_weight(FontWeight::SEMIBOLD)
                        .on_press(move || paused.update(|p| *p = !*p)),
                )
                .child(link("Restart").on_press(move || player.seek(0.0)))
                .child(text(move || time.get()).text_color(rgb(0x57606a))),
        )
}

La riproduzione è controllata dalle prop: imposta paused, muted, volume e rate da signal e il player li segue. Il ref ha seek(seconds).

In Rust, video(url) crea il player (l’URL è un valore, un signal o una closure) e ogni prop è un metodo builder in snake case (paused, repeat(true), object_fit(ResizeMode::Contain) (lo stile objectFit), progress_interval per progressUpdateInterval); on_load riceve un VideoInfo (duration, size), on_progress un VideoProgress (current_time, duration) e on_buffer un bool.

PropTipoDefaultDescrizione
srcstringL’URL del video.
pausedbooleanfalseLa riproduzione è in pausa.
mutedbooleanfalseNessun audio.
volumenumber10..1.
ratenumber1La velocità di riproduzione.
repeatbooleanfalseAlla fine riparte dall’inizio.
objectFit (style), classi object-*"cover" | "contain" | "fill" | "none" | "scale-down""contain"Come l’immagine si adatta alla view, come object-fit di CSS (class="object-cover").
controlsbooleanfalseI controlli di riproduzione della piattaforma.
progressUpdateIntervalnumber250Millisecondi tra gli eventi onProgress.
EventoPayloadQuando
onLoadStartIl caricamento è iniziato.
onLoad{ duration, size }Il video è pronto: durata in secondi e dimensioni.
onProgress{ currentTime, duration }Durante la riproduzione, ogni progressUpdateInterval ms.
onBuffer{ isBuffering }La riproduzione si è bloccata in attesa di dati, oppure è ripresa.
onEndLa riproduzione è arrivata alla fine.
onError{ message }Caricamento o riproduzione non riusciti.

Con controls, la piattaforma disegna il proprio pulsante play, lo scrubber e il pulsante a schermo intero. Senza, disegna i tuoi con view sopra o sotto il video; poiché il video è una view nativa nel tuo layout, gli overlay funzionano in modo naturale:

view()
    .h(px(220.0))
    .child(video(url).paused(paused).flex_1())
    .child(
        view()
            .absolute()
            .inset_0()
            .items_center()
            .justify_center()
            .on_press(move || paused.update(|p| *p = !*p))
            .child(when(move || paused.get(), || {
                text("▶").text_size(px(48.0)).text_color(rgb(0xffffff))
            })),
    )

Le sorgenti video sono richieste di rete: con le capability dichiarate, l’host del video deve essere consentito in network di nsh.json, come per fetch e le immagini remote. Una sorgente rifiutata non mostra nulla, e il rifiuto viene registrato nel log una sola volta, con la chiave da aggiungere.

#Correlati