Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Документация

Устройство и окружение

Окружение, в котором работает приложение, меняется во время работы: окно меняет размер (поворот, split view, окно на Mac), пользователь включает тёмную тему или увеличивает размер текста, появляется и скрывается клавиатура. NSH предоставляет каждое из этих изменений как реактивный accessor: вызывайте его как сигнал, и всё, что его прочитало (стиль, текст, вычисляемое значение), обновится при изменении. Не нужно добавлять и удалять слушатели.

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

В Rust accessor’ы являются функциями nsh_core::prelude; в TSX они импортируются из перечисленных модулей.

RustTSXЗначение
window()windowSize() (nsh)Ширина и высота поверхности в пунктах.
dark()colorScheme() (nsh)Тёмная или светлая тема: системная или собственная тема приложения (set_color_scheme, setColorScheme).
font_scale()fontScale() (nsh)Предпочтительный пользователем размер текста относительно стандартного (1 — стандартный).
safe_area()safeArea() (nsh)Отступы поверхности, не закрытые системным UI: сверху, справа, снизу, слева.
cx.insets()safeAreaInsets() (nsh)Отступы там, где отрисовывается вызывающий компонент (страница, диалог).
keyboard_height(), keyboard_duration()keyboardFrame() (nsh)Высота экранной клавиатуры и длительность её анимации.
locale()locale() (nsh)Локаль пользователя в виде тега BCP 47 ("en-US", "zh-Hans-CN"): для неё по умолчанию форматирует Intl.
lifecycle()lifecycle() (nsh)Используется ли приложение: active, inactive (видно, но без фокуса) или background (В фоне).
reduce_motion(), screen_reader()reduceMotion(), screenReaderEnabled() (nsh)См. Accessibility.
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)),
        )
}

#Размер окна и адаптивная раскладка

window() (TSX: windowSize()) — размер поверхности, в которую NSH выполняет отрисовку: экран на телефоне, окно на iPad в split view или на Mac, host view при встраивании NSH. Используйте его, чтобы менять раскладку, а не только размеры:

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

Часто flexbox, проценты и aspect_ratio (aspectRatio) решают задачу вообще без чтения размера; см. Layout. Rust-функция style_when(|| window().width.0 >= 640.0, |s| s.flex_row()) переключает стили в зависимости от ширины; в TSX с Tailwind варианты breakpoint’ов (sm:, md:, lg:, xl:, 2xl:, max-*:, min-[…]:) и portrait: / landscape: тоже следуют за окном: class="grid grid-cols-2 md:grid-cols-4".

#Тёмная тема

Внешний вид следует за системным, пока приложение не задаст собственный. В Rust проще всего поддержать тёмную тему с помощью цвета со светлым и тёмным значениями, AdaptiveColor::rgb(light, dark): он переключается нативно вместе с внешним видом, и ничего не нужно запускать. В TSX для этого служит вариант Tailwind dark:, который применяется, когда colorScheme() равно "dark":

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

Во всех остальных случаях читайте dark() (TSX: colorScheme()) в замыкании или вычисляемом значении; в TSX, если цветов больше нескольких, один раз выведите палитру:

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

Компоненты Rust также берут цвета из темы со светлым и тёмным значениями (nsh.json "theme", см. Building with Rust), поэтому они тоже переключаются вместе с системой. Чтобы привести строку состояния в соответствие с фоном, добавьте своим страницам и диалогам statusBar или задайте его для всего приложения в nsh.json.

#Приложение со своим внешним видом

Приложение, которое всегда тёмное (или всегда светлое), указывает это в nsh.json, и dark() и colorScheme(), адаптивные цвета, классы dark:, тема, стиль строки состояния по умолчанию и алерты следуют ему с первого кадра (упакованные приложения тоже запускаются в нём):

{ "appearance": "dark" }

set_color_scheme(Appearance::Dark) (TSX: setColorScheme из nsh) меняет его во время работы, например для настройки внешнего вида внутри приложения; Appearance::System ("system") снова следует за системой:

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

#Размер текста

font_scale() (TSX: fontScale()) — настройка размера текста пользователя (Dynamic Type в iOS, размер шрифта в Android) относительно стандартного. Текст и поля ввода следуют ей сами: fontSize: 16 отображается крупнее, если пользователь запросил более крупный текст. Для отдельного элемента можно отказаться от этого или ограничить масштаб:

Rust (на text и input)TSX (на <Text> и <Input>)По умолчаниюОписание
.allow_font_scaling(..)allowFontScalingtrueСледовать системному размеру текста.
.max_font_size_multiplier(..)maxFontSizeMultiplier0 (нет)Наибольший масштаб, применяемый к этому тексту.
text("Today")
    .text_size(px(34.0))
    .font_weight(FontWeight::BOLD)
    .max_font_size_multiplier(1.4)

Читайте font_scale(), чтобы адаптировать раскладку, которая не следует за текстом автоматически, например переключать строку в колонку, когда текст становится крупным: .style_when(|| font_scale() > 1.3, |s| s.flex_col()) в Rust, flexDirection: fontScale() > 1.3 ? "column" : "row" в TSX.

Размер текста масштабирует текст: каждый текст и поле ввода, допускающие масштабирование шрифта, а также платформенные алерты и тосты. Длины сохраняют свой размер: отступы, ширина и высота, радиусы скругления, границы и изображения, заданные в пунктах или классами (p-4, rounded-lg, w-[2rem]). Блоки, размер которых определяется содержимым, растут вместе с текстом.

#Язык и регион

locale() — локаль пользователя в виде тега BCP 47 (locale().as_str() в Rust). В TSX Intl по умолчанию форматирует для неё числа и даты, поэтому new Intl.NumberFormat().format(1234.5) даёт 1,234.5 в en-US и 1.234,5 в de-DE. Читайте locale(), чтобы выбирать строки собственных переводов: тексты, которые его читают, обновляются при изменении.

use nsh_core::prelude::*;

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

text(greeting)

#Во встраивающем приложении

Приложение, показанное внутри нативного приложения, следует его собственным настройкам, если оно их задаёт: локаль и размер текста из API встраивания (AppView::set_locale и set_font_scale в Rust на GPUI, macOS, Linux и Windows, locale и fontScale в Swift на iOS, setLocale и setFontScale в Java на Android) заменяют системные язык и размер текста. locale(), Intl, fontScale() и текст следуют им сразу, как и системным изменениям, а системные значения возвращаются, когда встраивающее приложение сбрасывает свои. Используйте их, если нативное приложение позволяет пользователям выбирать собственный язык или размер текста.

#В фоне

lifecycle() (то же в TSX) сообщает, использует ли пользователь приложение. Оно равно active на переднем плане, inactive, пока приложение ещё видно, но фокус у другого приложения или окна (или система показывает что-то поверх него: переключатель приложений, входящий звонок), и background, пока оно не видно: в фоне на телефоне, свёрнуто на десктопе, скрытая вкладка в вебе. Как и любой accessor, он обновляет то, что его прочитало, поэтому эффект приостанавливает живую ленту, опрос или видео в фоне и возобновляет их, когда пользователь возвращается:

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

Используйте background, чтобы приостанавливать работу, и сохраняйте изменения пользователя по мере их внесения: система может приостановить приложение через секунды после перехода в фон, раньше, чем выполнится эффект, читающий это изменение. Приложение, показанное внутри нативного приложения, следует состоянию того приложения, а если у приложения несколько окон (iPad, десктоп), представление следует за окном, в котором находится. В тестах app.setLifecycle("background") из nsh/test переводит приложение в это состояние (Testing).

#Безопасные области

Вырезы, индикатор «Домой», строка состояния, панель навигации Android и заголовок окна Mac Catalyst закрывают части поверхности. Приложения NSH рисуют от края до края на всех платформах, а закрытые части получают в виде отступов.

  • safe_area() (TSX: safeArea()) возвращает отступы поверхности.
  • cx.insets() компонента (TSX: safeAreaInsets()) возвращает отступы там, где компонент отрисовывается: внутри навигационной страницы они измеряются ниже её заголовка, внутри диалога это отступы диалога. Предпочитайте его в компонентах, которые могут показываться в разных местах.
  • CSS-функции env(safe-area-inset-top), -right, -bottom и -left задают внутренние или внешние отступы любого представления по отступам там, где оно отрисовывается, в padding* и margin* (calc(env(safe-area-inset-bottom) + 8px) добавляет длину; Tailwind pt-[env(safe-area-inset-top)]; Rust .p_safe_area(), .pt_safe_area(), .pb_safe_area_plus(px(8.0)), …). Padding обновляется нативно при изменении отступов (Safe area).
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))
}

В Android представление, встроенное под панелью инструментов, получает только те части системных панелей, которые его перекрывают, как safe area в UIKit: если оно не доходит до строки состояния, верхнего отступа нет.

#Платформа

os(), os_version(), idiom(), is_desktop() и is_catalyst() в Rust и константа platform из nsh в TSX описывают, где работает приложение. Они не меняются, пока приложение работает.

RustTSXОписание
os()platform.osOs::Ios, Os::Android, Os::MacOs, Os::Windows, Os::Linux или Os::Web (os().name() — строка в TSX). Mac Catalyst — это iOS.
os_version()platform.versionСтаршая и младшая версии; в Android — уровень API и 0.
idiom()platform.idiomIdiom::Phone, Pad, Mac, Tv или Desktop; Desktop ровно там, где выполняется is_desktop().
is_desktop()platform.desktopmacOS, Windows или Linux либо десктопный браузер (Desktop and mobile).
is_catalyst()platform.catalystMac Catalyst, в любом из двух его идиомов.

platform в TSX полностью:

ЧленОписание
platform.os"ios", "android", "macos", "windows", "linux" или "web". Mac Catalyst сообщает "ios".
platform.versionСтрока: iOS и десктопы: версия ОС ("17.5"); Android: уровень API ("35"); веб: "0.0".
platform.idiom"phone", "pad", "mac", "tv" или "desktop". "desktop" ровно там, где platform.desktop равно true. "mac" — это идиом Mac в UIKit под Mac Catalyst, мобильный идиом.
platform.desktopПрименяется поведение десктопа: macOS, Windows, Linux и десктопные браузеры. Props desktop={{…}} и platform.select({ desktop }) следуют ему.
platform.catalystПриложение работает под Mac Catalyst (os "ios"), в идиоме Mac или iPad.
platform.select(spec)Значение для этой платформы; см. Platform-specific code.
use nsh_core::{element::Os, prelude::*};

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

nsh test работает в headless-режиме как iOS.

На Linux platform.os равно "linux", идиом — "desktop", а platform.version — старшая и младшая версии ядра ("6.12"): дистрибутивы нумеруют свои релизы так, что их нельзя сравнивать. Там safe area и высота клавиатуры равны 0, colorScheme() следует настройке рабочего стола (XDG settings portal) в реальном времени, fontScale() — масштабированию текста рабочего стола, а pixelRatio() — масштабу дисплея, включая дробные значения.

На macOS (бэкенд AppKit) platform.os равно "macos", идиом — "desktop", а platform.version — версия macOS. Safe area и высота клавиатуры равны 0, colorScheme() следует системному внешнему виду в реальном времени, fontScale() равно 1, а pixelRatio() — backing scale дисплея.

На Windows platform.os равно "windows", идиом — "desktop", а platform.version — "10.0", в том числе в Windows 11 (Windows сохранила номер версии). Safe area и высота клавиатуры равны 0, colorScheme() следует настройке «Параметры > Персонализация > Цвета» в реальном времени, fontScale() — настройке «Размер текста» (от 1 до 2,25), reduceMotion() — настройке эффектов анимации, pixelRatio() — масштабу монитора, а Intl форматирует по региональным настройкам пользователя.

В браузере (веб-рантайм) platform.os равно "web", а platform.version — "0.0". Идиом — "desktop" при наличии мыши и окна шириной не менее 600 пунктов, "pad" на планшете и "phone" на телефоне; он определяется при запуске приложения. Локаль, reduceMotion(), fontScale() (размер текста браузера), pixelRatio(), safe area и высота клавиатуры берутся из браузера и следуют за ним в реальном времени.

#Плотность пикселей

pixel_ratio() и round_to_nearest_pixel(points) в Rust и pixelRatio() из nsh в TSX преобразуют пункты в пиксели устройства и обратно.

RustTSXОписание
pixel_ratio()pixelRatio()Пикселей устройства на пункт (2 или 3 на большинстве телефонов), реактивно.
round_to_nearest_pixel(points)Math.round(x * pixelRatio()) / pixelRatio()Пункты, округлённые до ближайшего пикселя устройства.

Тонкая линия высотой в один пиксель устройства:

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

pixel_ratio() и pixelRatio() реактивны, поэтому тонкая линия следует за окном, перемещённым на экран с другой плотностью.

#Справа налево

Когда язык устройства пишется справа налево (арабский, иврит, персидский, …), NSH выстраивает приложение справа налево: строки идут от правого края, логические свойства стиля принимают свои правые-левые стороны, текст выравнивается по правому краю, а нативные виджеты зеркально отражаются. Направление раскладки само следует за языком пользователя. Стиль direction представления ("ltr", "rtl" или "inherit"; .direction(LayoutDirection::..) в Rust) задаёт направление его поддерева; на корневом представлении он задаёт направление всего приложения.

Изменения применяются сразу, без перезапуска приложения; демо на этой странице переключает стиль direction.

Пишите раскладки, которые работают в обоих направлениях, с помощью логических свойств: в Rust ms / me и ps / pe (имена из Tailwind), в TSX marginStart / marginEnd, paddingStart / paddingEnd, start / end, borderTopStartRadius, textAlign: "start" и так далее; они означают левую и правую стороны в раскладках слева направо и наоборот в раскладках справа налево. left и right (marginLeft, paddingRight, left, textAlign: "left", …) сохраняют свои стороны в обоих случаях, как в CSS. Стиль direction ("inherit", "ltr", "rtl") переопределяет направление для поддерева, например чтобы оставить номер телефона или элементы управления медиаплеера слева направо. Варианты Tailwind rtl: и ltr: применяются для каждого направления отдельно.

Горизонтальные scroll view и списки в раскладках справа налево начинаются с правого края. В Android приложение-хост должно объявить android:supportsRtl="true" в манифесте, чтобы нативные виджеты зеркально отражались.

#См. также