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

Documentação

Dispositivo e ambiente

O ambiente em que um app roda muda enquanto ele roda: a janela é redimensionada (rotação, tela dividida, uma janela do Mac), o usuário muda para o modo escuro ou para um tamanho de texto maior, o teclado aparece e some. O NSH expõe cada um desses casos como um accessor reativo: chame-o como um signal, e o que o leu (um estilo, um texto, um valor computado) é atualizado quando ele muda. Não há listeners para adicionar ou remover.

guide-device.tsx
guide-device.tsxLive
import { mount, signal, Text, View } from "nsh";
import { colorScheme, fontScale, pixelRatio, platform, windowSize } from "nsh";

function Row(props: { label: string; value: string }) {
  return (
    <View class="flex-row py-2 border-b border-[#e9e9e4] dark:border-[#1f2124]">
      <Text class="flex-1 text-[#565a61] dark:text-[#8d9198]">{props.label}</Text>
      <Text class="text-[#15161a] dark:text-[#eceef1]">{props.value}</Text>
    </View>
  );
}

function App() {
  const hairline = () => 1 / pixelRatio();
  const rtl = signal(false);
  return (
    <View class="flex-1 bg-[#f3f3ef] dark:bg-[#101113] px-4 pt-6 gap-4">
      <Text class="text-2xl font-bold text-[#15161a] dark:text-[#eceef1]">Environment</Text>
      <View class="rounded-2xl bg-white border border-[#e1e1db] dark:bg-[#18191c] dark:border-[#2c2e33] px-4 py-1">
        <Row label="windowSize()" value={`${Math.round(windowSize().width)} × ${Math.round(windowSize().height)}`} />
        <Row label="colorScheme()" value={colorScheme()} />
        <Row label="fontScale()" value={fontScale().toFixed(2)} />
        <Row label="platform.os" value={platform.os} />
        <Row label="platform.version" value={platform.version} />
        <Row label="platform.idiom" value={platform.idiom} />
        <Row label="platform.desktop" value={String(platform.desktop)} />
        <Row label="Locale" value={new Intl.DateTimeFormat().resolvedOptions().locale} />
        <Row label="pixelRatio()" value={String(pixelRatio())} />
      </View>

      <Text class="text-[#565a61] dark:text-[#8d9198]">
        {platform.select({ ios: "Running on iOS.", android: "Running on Android.", web: "Running in a browser.", default: "Running elsewhere." })}
      </Text>

      <View class="gap-1">
        <View class="bg-[#8c9097] dark:bg-[#5a5e65]" style={{ height: hairline() }} />
        <Text class="text-xs text-[#8c9097] dark:text-[#5a5e65]">The line above is one device pixel tall.</Text>
      </View>

      <View class={windowSize().width >= 600 ? "flex-row gap-3" : "gap-3"}>
        <View class="flex-1 rounded-xl bg-[#4a6a0a] dark:bg-[#c8f26d] p-4">
          <Text class="text-white dark:text-[#13160c] font-semibold">dark: classes follow the system</Text>
        </View>
        <View class="flex-1 rounded-xl bg-white border border-[#e1e1db] dark:bg-[#24262a] dark:border-[#2c2e33] p-4 hover:bg-zinc-50 dark:hover:bg-[#1d1e22] focus-visible:outline-2 focus-visible:outline-sky-500">
          <Text class="text-[#15161a] dark:text-[#eceef1] font-semibold">A row only when the window is 600 pt or wider</Text>
        </View>
      </View>

      <View
        onPress={() => rtl.set(!rtl())}
        activeStyle={{ opacity: 0.6 }}
        style={{ direction: rtl() ? "rtl" : "ltr" }}
        class="flex-row items-center gap-3 rounded-2xl bg-white border border-[#e1e1db] dark:bg-[#18191c] dark:border-[#2c2e33] p-4 hover:bg-zinc-50 dark:hover:bg-[#1d1e22] focus-visible:outline-2 focus-visible:outline-sky-500"
      >
        <Text class="text-lg text-[#4a6a0a] dark:text-[#c8f26d]">⇄</Text>
        <Text class="flex-1 text-[#15161a] dark:text-[#eceef1]">Lay out right to left: {rtl() ? "on" : "off"}</Text>
      </View>
    </View>
  );
}

mount(App);

Em Rust, os accessors são funções de nsh_core::prelude; em TSX, vêm dos módulos listados.

Rust TSX Valor
window() windowSize() (nsh) Largura e altura da superfície, em pontos.
dark() colorScheme() (nsh) Escuro ou claro: o do sistema, ou o próprio do app (set_color_scheme, setColorScheme).
font_scale() fontScale() (nsh) O tamanho de texto preferido do usuário em relação ao padrão (1 é o padrão).
safe_area() safeArea() (nsh) Insets da superfície não cobertos pela UI do sistema: superior, direito, inferior, esquerdo.
cx.insets() safeAreaInsets() (nsh) Os insets de onde o componente que chama é renderizado (uma página, um diálogo).
keyboard_height(), keyboard_duration() keyboardFrame() (nsh) A altura e a duração da animação do teclado virtual.
locale() locale() (nsh) O locale do usuário como tag BCP 47 ("en-US", "zh-Hans-CN"): o formato que Intl usa por padrão.
lifecycle() lifecycle() (nsh) Se o app está em uso: active, inactive (visível sem o foco) ou background (Em segundo plano).
reduce_motion(), screen_reader() reduceMotion(), screenReaderEnabled() (nsh) Veja Acessibilidade.
use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    view()
        .flex_1()
        .pt(move || safe_area().top + px(16.0))
        .px(px(16.0))
        // A light and a dark value: the color follows the appearance natively.
        .bg(AdaptiveColor::rgb(0xffffff, 0x0d1117))
        .child(
            text(move || {
                let size = window();
                format!(
                    "{} × {} pt, text at {:.2}×",
                    size.width.0.round(),
                    size.height.0.round(),
                    font_scale()
                )
            })
            .text_color(AdaptiveColor::rgb(0x1f2328, 0xe6edf3)),
        )
}

#Tamanho da janela e layout responsivo

window() (TSX: windowSize()) é o tamanho da superfície em que o NSH renderiza: a tela em um celular, a janela em um iPad com tela dividida ou em um Mac, a view hospedeira quando o NSH está incorporado. Use-o para mudar o layout, não apenas os tamanhos:

use nsh_core::prelude::*;

fn gallery() -> impl IntoElement {
    let columns = || {
        let width = window().width.0;
        if width >= 700.0 { 4.0 } else if width >= 500.0 { 3.0 } else { 2.0 }
    };
    let side = move || px((window().width.0 - 32.0 - (columns() - 1.0) * 8.0) / columns());
    view().flex_row().flex_wrap().gap_2().child(each(
        photos,
        |p| p.id,
        move |p, _| image(move || p.with(|p| p.url.clone())).w(side).aspect_ratio(1.0),
    ))
}

Muitas vezes flexbox, porcentagens e aspect_ratio (aspectRatio) resolvem sem precisar ler o tamanho; veja Layout. O style_when(|| window().width.0 >= 640.0, |s| s.flex_row()) do Rust troca estilos conforme a largura; em TSX com Tailwind, as variantes de breakpoint (sm:, md:, lg:, xl:, 2xl:, max-*:, min-[…]:) e portrait: / landscape: também acompanham a janela: class="grid grid-cols-2 md:grid-cols-4".

#Modo escuro

A aparência segue o sistema, a menos que o app defina a sua própria. Em Rust, a forma mais simples de oferecer suporte ao modo escuro é uma cor com um valor claro e um escuro, AdaptiveColor::rgb(light, dark): ela muda nativamente quando a aparência muda, sem nada para executar. Em TSX, é a variante dark: do Tailwind, que se aplica quando colorScheme() é "dark":

view()
    .flex_1()
    .p_4()
    .bg(AdaptiveColor::rgb(0xffffff, 0x18181b))
    .child(
        text("Follows the system appearance")
            .text_color(AdaptiveColor::rgb(0x18181b, 0xf4f4f5)),
    )

Para todo o resto, leia dark() (TSX: colorScheme()) em um closure ou em um valor computado; em TSX, para mais do que algumas cores, derive uma paleta uma só vez:

use nsh_core::prelude::*;

// `dark()` is reactive: the closure runs again when the appearance changes.
view()
    .flex_1()
    .bg(move || if dark() { rgb(0x0d1117) } else { rgb(0xf6f8fa) })
    .child(text("Hello"))

Os componentes Rust também tiram suas cores do tema, que tem valores claros e escuros (nsh.json "theme", veja Criando com Rust), então eles também acompanham o sistema. Para combinar a barra de status com o seu fundo, dê às suas páginas e diálogos um statusBar, ou dê um ao app em nsh.json.

#Um app com aparência própria

Um app que é sempre escuro (ou sempre claro) declara isso em nsh.json, e dark() e colorScheme(), as cores adaptativas, as classes dark:, o tema, o estilo padrão da barra de status e os alertas o seguem desde o primeiro frame (apps empacotados também são iniciados nele):

{ "appearance": "dark" }

set_color_scheme(Appearance::Dark) (TSX: setColorScheme de nsh) o altera em tempo de execução, para uma configuração de aparência dentro do app; Appearance::System ("system") volta a seguir o sistema:

use nsh_core::prelude::*;

view()
    .on_press(move || {
        set_color_scheme(if dark() { Appearance::Light } else { Appearance::Dark })
    })
    .role(AccessibilityRole::Button)
    .rounded_lg()
    .px_4()
    .py_2()
    .bg(AdaptiveColor::rgb(0xe5e5e5, 0x262626))
    .child(
        text(|| if dark() { "Light" } else { "Dark" })
            .text_color(AdaptiveColor::rgb(0x171717, 0xfafafa)),
    )

#Tamanho do texto

font_scale() (TSX: fontScale()) é a configuração de tamanho de texto do usuário (Dynamic Type no iOS, tamanho da fonte no Android) em relação ao padrão. Textos e inputs a seguem por conta própria: fontSize: 16 é renderizado maior quando o usuário pediu um texto maior. Por elemento, você pode desativar isso ou limitar o valor:

Rust (em text e input) TSX (em <Text> e <Input>) Padrão Descrição
.allow_font_scaling(..) allowFontScaling true Segue o tamanho de texto do sistema.
.max_font_size_multiplier(..) maxFontSizeMultiplier 0 (nenhum) A maior escala aplicada a este texto.
text("Today")
    .text_size(px(34.0))
    .font_weight(FontWeight::BOLD)
    .max_font_size_multiplier(1.4)

Leia font_scale() para adaptar o layout que não acompanha o texto automaticamente, como trocar uma linha por uma coluna quando o texto fica grande: .style_when(|| font_scale() > 1.3, |s| s.flex_col()) em Rust, flexDirection: fontScale() > 1.3 ? "column" : "row" em TSX.

O tamanho do texto escala texto: todo texto e input que permite escala de fonte, além dos alertas e toasts da plataforma. Os comprimentos mantêm o tamanho: espaçamentos, larguras e alturas, raios de canto, bordas e imagens, em pontos ou em classes (p-4, rounded-lg, w-[2rem]). Caixas dimensionadas pelo conteúdo crescem junto com o texto.

#Idioma e região

locale() é o locale do usuário, como uma tag BCP 47 (locale().as_str() em Rust). Em TSX, Intl formata números e datas para ele por padrão, de modo que new Intl.NumberFormat().format(1234.5) resulta em 1,234.5 em en-US e 1.234,5 em de-DE. Leia locale() para escolher as strings das suas próprias traduções: os textos que o leem são atualizados quando ele muda.

use nsh_core::prelude::*;

fn greeting() -> &'static str {
    match locale().as_str().split('-').next() {
        Some("de") => "Willkommen",
        Some("zh") => "欢迎",
        _ => "Welcome",
    }
}

text(greeting)

#Em um app hospedeiro

Um app exibido dentro de um app nativo segue as configurações do próprio app hospedeiro quando ele as define: o locale e o tamanho de texto das APIs de incorporação (AppView::set_locale e set_font_scale em Rust no GPUI, macOS, Linux e Windows, locale e fontScale em Swift no iOS, setLocale e setFontScale em Java no Android) substituem o idioma e o tamanho de texto do sistema. locale(), Intl, fontScale() e o texto os seguem imediatamente, como seguem uma mudança do sistema, e os valores do sistema voltam quando o app hospedeiro os limpa. Use-os quando o app nativo deixa seus usuários escolherem um idioma ou um tamanho de texto próprios.

#Em segundo plano

lifecycle() (o mesmo em TSX) diz se o usuário está usando o app. É active em primeiro plano, inactive enquanto o app ainda está visível mas outro app ou janela tem o foco (ou o sistema mostra algo sobre ele: o seletor de apps, uma chamada recebida), e background enquanto ele não está visível: em segundo plano em um celular, minimizado em um desktop, uma aba oculta na web. Como todo accessor, ele atualiza o que o leu, então um effect pausa um feed ao vivo, um polling ou um vídeo em segundo plano e os retoma quando o usuário volta:

effect(move || {
    if lifecycle() == Lifecycle::Background {
        feed.pause();
    } else {
        feed.resume();
    }
});

Use background para pausar trabalho e salve o que o usuário altera conforme ele altera: o sistema pode suspender um app segundos depois de ele ir para segundo plano, antes que um effect que lê a mudança tenha sido executado. Um app exibido dentro de um app nativo segue o estado desse app, e, onde um app tem várias janelas (um iPad, um desktop), uma view segue a janela em que está. Em testes, app.setLifecycle("background") de nsh/test leva o app para esse estado (Testes).

#Áreas seguras

Notches, o indicador de início, a barra de status, a barra de navegação do Android e a barra de título do Mac Catalyst cobrem partes da superfície. Os apps NSH desenham de ponta a ponta em todas as plataformas e recebem as partes cobertas como insets.

  • safe_area() (TSX: safeArea()) fornece os insets da superfície.
  • O cx.insets() de um componente (TSX: safeAreaInsets()) fornece os insets de onde o componente é renderizado: dentro de uma página de navegação, eles são medidos abaixo do cabeçalho; dentro de um diálogo, são os do diálogo. Prefira-o em componentes que podem ser exibidos em vários lugares.
  • env(safe-area-inset-top), -right, -bottom e -left do CSS aplicam padding ou espaçamento a qualquer view conforme os insets de onde ela é renderizada, em padding* e margin* (calc(env(safe-area-inset-bottom) + 8px) soma um comprimento; Tailwind pt-[env(safe-area-inset-top)]; Rust .p_safe_area(), .pt_safe_area(), .pb_safe_area_plus(px(8.0)), …). O padding é atualizado nativamente quando os insets mudam (Área segura).
use nsh_core::prelude::*;

fn screen() -> impl IntoElement {
    view()
        .pt_safe_area()
        .px_safe_area()
        .flex_1()
        .bg(white())
        .child(content())
        .child(bottom_bar())
}

// A component, for its `Scope`: the insets where it renders.
#[component]
fn BottomBar(cx: Scope<NoEvent>) -> impl IntoElement {
    // `cx.insets()`: the insets where the component renders.
    view()
        .flex_row()
        .pt(px(8.0))
        .pb(move || cx.insets().bottom + px(8.0))
}

No Android, uma view incorporada abaixo de uma toolbar recebe apenas as partes das barras do sistema que se sobrepõem a ela, como a área segura do UIKit: nenhum inset superior se ela não alcança a barra de status.

#A plataforma

os(), os_version(), idiom(), is_desktop() e is_catalyst() em Rust, e a constante platform de nsh em TSX, descrevem onde o app roda. Eles permanecem os mesmos enquanto o app roda.

Rust TSX Descrição
os() platform.os Os::Ios, Os::Android, Os::MacOs, Os::Windows, Os::Linux ou Os::Web (os().name() é a string do TSX). Mac Catalyst é iOS.
os_version() platform.version Maior e menor; no Android, o nível da API e 0.
idiom() platform.idiom Idiom::Phone, Pad, Mac, Tv ou Desktop; Desktop exatamente onde is_desktop() vale.
is_desktop() platform.desktop macOS, Windows ou Linux, ou um navegador desktop (Desktop e mobile).
is_catalyst() platform.catalyst Mac Catalyst, em qualquer um de seus idioms.

platform do TSX, completo:

Membro Descrição
platform.os "ios", "android", "macos", "windows", "linux" ou "web". O Mac Catalyst informa "ios".
platform.version Uma string: no iOS e nos desktops, a versão do SO ("17.5"); no Android, o nível da API ("35"); na web, "0.0".
platform.idiom "phone", "pad", "mac", "tv" ou "desktop". "desktop" exatamente onde platform.desktop é true. "mac" é o idiom Mac do UIKit sob o Mac Catalyst, um idiom mobile.
platform.desktop O comportamento de desktop se aplica: macOS, Windows, Linux e navegadores desktop. As props desktop={{…}} e platform.select({ desktop }) o seguem.
platform.catalyst O app roda sob o Mac Catalyst (os "ios"), no idiom Mac ou iPad.
platform.select(spec) O valor para esta plataforma; veja Código específico de plataforma.
use nsh_core::{element::Os, prelude::*};

let haptics_supported = os() == Os::Ios || (os() == Os::Android && os_version().0 >= 30);

nsh test roda headless como iOS.

No Linux, platform.os é "linux", o idiom é "desktop" e platform.version é a versão maior e menor do kernel ("6.12"): as distribuições numeram suas versões de formas que não podem ser comparadas. Lá, a área segura e a altura do teclado são 0, colorScheme() segue ao vivo a configuração do desktop (o portal de configurações XDG), fontScale() segue o escalonamento de texto do desktop e pixelRatio() segue a escala da tela, inclusive as fracionárias.

No macOS (o backend AppKit), platform.os é "macos", o idiom é "desktop" e platform.version é a versão do macOS. A área segura e a altura do teclado são 0, colorScheme() segue ao vivo a aparência do sistema, fontScale() é 1 e pixelRatio() é a escala de backing da tela.

No Windows, platform.os é "windows", o idiom é "desktop" e platform.version é "10.0", também no Windows 11 (o Windows manteve o número da versão). A área segura e a altura do teclado são 0, colorScheme() segue ao vivo Configurações > Personalização > Cores, fontScale() segue a configuração Tamanho do texto (1 a 2,25), reduceMotion() segue a configuração de efeitos de animação, pixelRatio() segue a escala do monitor e Intl formata no formato regional do usuário.

Em um navegador (o runtime web), platform.os é "web" e platform.version é "0.0". O idiom é "desktop" com um mouse e uma janela de pelo menos 600 pontos de largura, "pad" em um tablet e "phone" em um celular, decidido quando o app é iniciado. O locale, reduceMotion(), fontScale() (o tamanho de texto do navegador), pixelRatio(), a área segura e a altura do teclado vêm do navegador e o acompanham ao vivo.

#Densidade de pixels

pixel_ratio() e round_to_nearest_pixel(points) em Rust, e pixelRatio() de nsh em TSX, convertem entre pontos e pixels do dispositivo.

Rust TSX Descrição
pixel_ratio() pixelRatio() Pixels do dispositivo por ponto (2 ou 3 na maioria dos celulares), reativo.
round_to_nearest_pixel(points) Math.round(x * pixelRatio()) / pixelRatio() Pontos arredondados para o pixel do dispositivo mais próximo.

Uma linha fina, com a altura de um pixel do dispositivo:

view()
    .h(move || px(1.0 / pixel_ratio()))
    .bg(rgb(0xd0d7de))

pixel_ratio() e pixelRatio() são reativos, então uma linha fina acompanha uma janela movida para uma tela com outra densidade.

#Da direita para a esquerda

Quando o idioma do dispositivo é escrito da direita para a esquerda (árabe, hebraico, persa, …), o NSH organiza o app da direita para a esquerda: as linhas começam pela direita, as propriedades de estilo lógicas assumem seus lados da direita para a esquerda, o texto se alinha à direita e os widgets nativos são espelhados. A direção do layout segue o idioma do usuário por conta própria. O estilo direction de uma view ("ltr", "rtl" ou "inherit"; .direction(LayoutDirection::..) em Rust) define a direção da sua subárvore; na view raiz, define a do app.

As mudanças são aplicadas imediatamente, sem reiniciar o app; a demonstração desta página alterna o estilo direction.

Escreva layouts que funcionem nos dois sentidos com as propriedades lógicas: em Rust ms / me e ps / pe (os nomes do Tailwind), em TSX marginStart / marginEnd, paddingStart / paddingEnd, start / end, borderTopStartRadius, textAlign: "start" e assim por diante, que significam esquerda e direita em layouts da esquerda para a direita e o inverso da direita para a esquerda. left e right (marginLeft, paddingRight, left, textAlign: "left", …) mantêm seus lados nos dois, como no CSS. O estilo direction ("inherit", "ltr", "rtl") sobrescreve a direção de uma subárvore, por exemplo para manter um número de telefone ou os controles de um player de mídia da esquerda para a direita. As variantes rtl: e ltr: do Tailwind se aplicam por direção.

Views de rolagem horizontal e listas começam na extremidade direita em layouts da direita para a esquerda. No Android, o app hospedeiro deve declarar android:supportsRtl="true" no manifest para que os widgets nativos sejam espelhados.

#Relacionado