Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Documentação

Web view e vídeo

A web view e o video player incorporam o mecanismo web e o player de mídia da própria plataforma como views comuns: web_view e video em Rust (nsh_core::prelude), WebView e Video de nsh em TSX. Como o NSH renderiza com views nativas, elas ficam no seu layout como qualquer outro elemento: rolam junto com o pai, aceitam cantos arredondados e recorte, e outras views podem sobrepô-las sem problemas de camadas.

Componente iOS Android macOS Linux Windows
WebView WKWebView android.webkit.WebView WKWebView WebKitWebView do WebKitGTK 6.0 WebView2, em uma janela filha
Video AVPlayerLayer; AVPlayerViewController com controls MediaPlayer em uma TextureView; MediaController com controls AVPlayerLayer; AVPlayerView do AVKit com controls media stream do GTK (GStreamer) em um GtkPicture; GtkVideo com controls media engine do Media Foundation, com quadros desenhados via Direct2D; barra de controles própria do NSH com controls

Os frameworks por trás deles (WebKit, AVFoundation) são carregados na primeira vez que um app usa algum deles, então apps que nunca exibem uma página web ou um vídeo não pagam esse custo na inicialização.

No macOS, a web view não tem uma scroll view própria: com scrollable={false}, a roda do mouse e o trackpad rolam as scroll views do app ao redor, em vez da página, que continua rolando pelo teclado e pelos próprios scripts.

No Linux, uma web view é uma página WebKitGTK desenhada dentro da sua view: conteúdo do NSH sobre ela (um diálogo, uma sheet) a cobre, e seus pais a recortam. A capacidade webview também se aplica às navegações de frames nessa plataforma (um iframe de um host não autorizado fica vazio), e scrollable: false impede que a própria página role (os elementos dentro dela continuam rolando). Os apps precisam do WebKitGTK 6.0 instalado; um runtime compilado sem ele mostra um placeholder neutro. Os vídeos reproduzem os formatos que os plugins GStreamer instalados decodificam; rate não é suportado (eles tocam a 1×), e onEnd só vem para vídeos que não estão em loop.

No Windows, uma web view é um controle WebView2 em uma janela filha que acompanha a sua view: ela é recortada pelos pais e pelos cantos arredondados, mas o conteúdo do NSH desenhado sobre ela (um diálogo, uma sheet) fica por baixo dela. Ela precisa do WebView2 Runtime (parte do Windows 11; instalado com o Edge no Windows 10); caso contrário, mostra um placeholder; baseUrl para HTML, scrollable e allowsBackForwardNavigationGestures não são suportados. Os vídeos são conteúdo comum (recortados, transformados e cobertos como qualquer view) e reproduzem os formatos que os codecs instalados do Media Foundation decodificam; rate funciona, não há poster, e a barra de controles sempre aparece enquanto controls está ativado.

#WebView

use nsh_core::prelude::*;

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

#Fontes

Uma web view mostra uma entre src e html:

Props Mostra
src="https://…" Uma página web.
html="<!doctype html>…" HTML a partir de uma string, mostrado no lugar de src.
html com baseUrl="https://example.com/" HTML cujas URLs relativas são resolvidas a partir de baseUrl.

Em Rust, web_view(url) mostra uma página e web_view_html(html, base_url) mostra HTML (base_url um Option<&str>); a URL e o HTML podem ser um signal ou uma closure, que recarrega a página quando o que ela lê muda. Alterar src ou html carrega a nova página.

#Props

Em Rust, cada prop é um método builder em snake case (injected_java_script, java_script_enabled, scrollable, user_agent; injected_java_script_before_content para injectedJavaScriptBeforeContentLoaded, back_forward_gestures para o allowsBackForwardNavigationGestures do iOS) e cada evento é um método on_… (on_navigation_state_change) cujo listener recebe um WebNavigation (url, title, loading, can_go_back, can_go_forward) ou, para on_error e on_message, um &str.

Prop Tipo Padrão Descrição
src string A URL da página a mostrar.
html string HTML inline, mostrado no lugar de src.
baseUrl string A base contra a qual as URLs relativas de html são resolvidas.
injectedJavaScript string Executa depois que cada página é carregada.
injectedJavaScriptBeforeContentLoaded string Executa antes dos scripts de cada página.
javaScriptEnabled boolean true As páginas executam scripts.
scrollable boolean true O usuário pode rolar a página.
userAgent string A string de user agent do navegador.
ios={{ allowsBackForwardNavigationGestures }} boolean iOS: deslizar para voltar e avançar no histórico da página.
desktop={{ allowsBackForwardNavigationGestures }} boolean macOS: deslizar com dois dedos no trackpad para voltar e avançar; Windows: deslizar em uma tela sensível ao toque; Linux: deslizar no touchpad.

Eventos:

Evento Payload Quando
onLoadStart WebViewNavigation Uma página começou a carregar.
onLoad WebViewNavigation Uma página terminou de carregar.
onError { message } O carregamento falhou.
onNavigationStateChange WebViewNavigation A URL, o título, o estado de carregamento ou o histórico mudou.
onMessage { data: string } A página enviou uma mensagem (abaixo).

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

#Um navegador com voltar e avançar

A ref da web view (um element_ref() colocado nela com .element_ref(&web) em Rust, ref em TSX) tem reload, go_back / goBack, go_forward / goForward, stop_loading / stopLoading, inject_java_script / injectJavaScript(script), post_message / postMessage(data) e send_input / sendInput(input) (abaixo):

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())),
        )
}

#Mensagens entre a página e o app

As páginas se comunicam com o app usando window.nsh.postMessage(string), que chega como on_message (onMessage). O app se comunica com a página usando o post_message da ref (postMessage(string)), que chega à página como um evento message em window. As mensagens são strings; envie JSON para dados estruturados.

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)))
}

O inject_java_script da ref (injectJavaScript(script)) executa código na página atual a qualquer momento; injected_java_script e injected_java_script_before_content (injectedJavaScript, injectedJavaScriptBeforeContentLoaded) executam a cada carregamento de página, antes ou depois dos scripts da própria página, que é o lugar para instalar hooks:

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}"))

#Entrada como se fosse do próprio usuário

injectJavaScript só consegue criar eventos sintéticos, que uma página vê como não confiáveis (event.isTrusted é false) e que alguns widgets ignoram. O send_input da ref (sendInput(input)) entrega à página uma entrada como se viesse das mãos do próprio usuário, de modo que seus eventos são confiáveis:

  • { type: "click", x, y } clica em um ponto da viewport, em pixels CSS (o que getBoundingClientRect() informa).
  • { type: "text", text } digita o texto no elemento que tem o foco da página, uma tecla por vez: cada caractere na tecla correspondente do layout US, uma quebra de linha como Enter, e o que nenhuma tecla US digita (é, CJK, emoji) em nenhuma tecla (code é "Unidentified").
  • { type: "key", key } pressiona Enter, Tab, Escape ou 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));
    });
}

Em Rust, estes são WebInput::Click { x, y }, WebInput::Text(text) e WebInput::Key(WebKey::Enter).

A entrada confiável carrega ativação do usuário (ela pode abrir janelas, escrever na área de transferência, abrir o painel de arquivos do sistema ou pressionar o botão de confirmação de uma página), então é uma capacidade à parte: o app precisa de "webview": { "input": true } em capabilities de nsh.json (webview), mesmo quando não declara nenhuma, e o core descarta a chamada caso contrário.

Como um clique, sendInput tira o foco do teclado do Input do próprio app e o dá à web view. A página trata a entrada no seu próprio ritmo: os eventos de um clique ainda podem estar a caminho quando a próxima chamada começa, então deixe o clique se assentar (um quadro, mais ou menos) antes de digitar depois dele. O macOS entrega a entrada; as outras plataformas a ignoram por enquanto.

Antes de oferecer qualquer coisa que dependa disso, pergunte se funciona aqui: web_input_available() (WebView.inputAvailable()) resolve true apenas onde a plataforma entrega a entrada e o app possui webview.input, portanto é false em plataformas que ignoram a chamada e em apps sem a concessão.

use nsh_core::prelude::*;

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

#Quais sites uma web view pode mostrar

Se o seu nsh.json declara "capabilities", uma web view só pode mostrar e navegar para os hosts listados em webview:

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

A verificação cobre o src da web view e toda navegação de nível superior feita pela página (links, redirecionamentos, alterações de location); ela é decidida nativamente quando a plataforma pergunta se uma navegação pode prosseguir. Os subrecursos da própria página (scripts, imagens, frames) são responsabilidade do conteúdo web e não são verificados. *.example.com corresponde apenas a subdomínios; liste também example.com para o domínio sem prefixo. Um app sem uma chave "capabilities" pode mostrar qualquer página. Veja Sandbox e capacidades.

#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))),
        )
}

A reprodução é controlada por props: defina paused, muted, volume e rate a partir de signals e o player acompanha. A ref tem seek(seconds).

Em Rust, video(url) cria o player (a URL sendo um valor, um signal ou uma closure) e cada prop é um método builder em snake case (paused, repeat(true), object_fit(ResizeMode::Contain) (o estilo objectFit), progress_interval para progressUpdateInterval); on_load recebe um VideoInfo (duration, size), on_progress um VideoProgress (current_time, duration) e on_buffer um bool.

Prop Tipo Padrão Descrição
src string A URL do vídeo.
paused boolean false A reprodução está pausada.
muted boolean false Sem som.
volume number 1 0..1.
rate number 1 A velocidade de reprodução.
repeat boolean false Reproduz de novo desde o início ao chegar ao fim.
objectFit (estilo), classes object-* "cover" | "contain" | "fill" | "none" | "scale-down" "contain" Como a imagem se ajusta à view, como o object-fit do CSS (class="object-cover").
controls boolean false Os controles de reprodução da plataforma.
progressUpdateInterval number 250 Milissegundos entre eventos onProgress.
Evento Payload Quando
onLoadStart O carregamento começou.
onLoad { duration, size } O vídeo está pronto: sua duração em segundos e seu tamanho.
onProgress { currentTime, duration } Durante a reprodução, a cada progressUpdateInterval ms.
onBuffer { isBuffering } A reprodução parou à espera de dados, ou foi retomada.
onEnd A reprodução chegou ao fim.
onError { message } O carregamento ou a reprodução falhou.

Com controls, a plataforma desenha seu próprio botão de reproduzir, barra de progresso e botão de tela cheia. Sem ele, desenhe os seus com views sobre ou sob o vídeo; como o vídeo é uma view nativa no seu layout, as sobreposições funcionam naturalmente:

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))
            })),
    )

As fontes de vídeo são requisições de rede: com capacidades declaradas, o host do vídeo deve ser permitido em network de nsh.json, assim como para fetch e imagens remotas. Uma fonte recusada não mostra nada, e a recusa é registrada no log uma única vez com a chave a adicionar.

#Relacionados