このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

ドキュメント

デバイスと環境

アプリが動作する環境は、実行中に変化します。ウィンドウのサイズ変更(回転、スプリットビュー、Mac のウィンドウ)、ダークモードや大きな文字サイズへの切り替え、キーボードの表示と非表示などです。NSH はそれぞれをリアクティブアクセサとして公開しています。シグナルのように呼び出すと、それを読み取ったもの(スタイル、テキスト、算出値)が変更時に更新されます。リスナーを追加・削除する必要はありません。

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 ではアクセサは 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)アクセシビリティを参照してください。
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 のスプリットビューや Mac ではウィンドウ、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),
    ))
}

多くの場合、フレックスボックス、パーセンテージ、aspect_ratio(aspectRatio)を使えば、サイズを読み取らなくても目的を達成できます。レイアウトを参照してください。Rust の style_when(|| window().width.0 >= 640.0, |s| s.flex_row()) は幅に応じてスタイルを切り替えます。TSX で Tailwind を使う場合は、ブレークポイントバリアント(sm:、md:、lg:、xl:、2xl:、max-*:、min-[…]:)と portrait: / landscape: もウィンドウに追従します。class="grid grid-cols-2 md:grid-cols-4"。

#ダークモード

外観は、アプリが独自に設定しない限りシステムに従います。Rust でダークモードに対応する最も簡単な方法は、ライト用とダーク用の値を持つ色 AdaptiveColor::rgb(light, dark) を使うことです。外観が変わるとネイティブに切り替わり、実行すべき処理はありません。TSX では dark: Tailwind バリアントを使います。これは 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"、Rust でのビルドを参照)から色を取得するため、システムに合わせて切り替わります。ステータスバーを背景に合わせるには、ページやダイアログに statusBar を指定するか、アプリ全体に対して nsh.json で指定します。

#独自の外観を持つアプリ

常にダーク(または常にライト)のアプリは nsh.json でそのことを宣言します。dark() と colorScheme()、アダプティブカラー、dark: クラス、テーマ、ステータスバーのデフォルトスタイル、アラートは、最初のフレームからそれに従います(パッケージ化されたアプリも、その外観で起動します)。

{ "appearance": "dark" }

set_color_scheme(Appearance::Dark)(TSX では nsh の setColorScheme)は、アプリ内の外観設定のために実行時に外観を変更します。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())は、デフォルトに対する相対値で表したユーザーの文字サイズ設定(iOS では Dynamic Type、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() を読み取ります。たとえば、文字が大きくなったときに行を列へ切り替える場合は、Rust では .style_when(|| font_scale() > 1.3, |s| s.flex_col())、TSX では flexDirection: fontScale() > 1.3 ? "column" : "row" を使います。

文字サイズによって拡大縮小されるのは、フォントのスケーリングを許可しているすべてのテキストと入力、およびプラットフォームのアラートとトーストです。長さはそのままのサイズを保ちます。ポイントまたはクラス(p-4、rounded-lg、w-[2rem])で指定した余白、幅と高さ、角丸、枠線、画像が該当します。内容に合わせてサイズが決まるボックスは、テキストとともに大きくなります。

#言語と地域

locale() は、BCP 47 タグで表したユーザーのロケール(Rust では locale().as_str())です。TSX では、Intl がデフォルトでそのロケールに合わせて数値と日付をフォーマットするため、new Intl.NumberFormat().format(1234.5) は en-US では 1,234.5、de-DE では 1.234,5 になります。独自の翻訳の文字列を選ぶには locale() を読み取ります。これを読み取っているテキストは、ロケールが変わると更新されます。

use nsh_core::prelude::*;

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

text(greeting)

#組み込みアプリでの動作

ネイティブアプリの内部に表示されるアプリは、そのアプリが独自の設定を行っている場合、その設定に従います。組み込み API のロケールと文字サイズ(Rust では GPUI、macOS、Linux、Windows 向けの AppView::set_locale と set_font_scale、Swift では iOS 向けの locale と fontScale、Java では Android 向けの setLocale と setFontScale)は、システムの言語と文字サイズの代わりに使われます。locale()、Intl、fontScale() とテキストは、システムの変更に追従するのと同様に、これらにすぐ追従します。組み込み元のアプリがそれらを解除すると、システムの設定に戻ります。ネイティブアプリがユーザーに独自の言語や文字サイズを選ばせる場合に使ってください。

#バックグラウンド時

lifecycle()(TSX でも同じ)は、ユーザーがアプリを使用中かどうかを表します。フォアグラウンドでは active、アプリがまだ表示されているものの別のアプリやウィンドウにフォーカスがある間(またはアプリスイッチャーや着信など、システムが上に何かを表示している間)は inactive、表示されていない間は background になります。表示されていない状態とは、スマートフォンではバックグラウンド、デスクトップでは最小化、Web では非表示のタブを指します。他のアクセサと同様に、これを読み取ったものが更新されるため、エフェクトでライブフィード、ポーリング、動画をバックグラウンドで一時停止し、ユーザーが戻ったときに再開できます。

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

background は処理の一時停止に使い、ユーザーが変更した内容は変更のたびに保存してください。システムは、アプリがバックグラウンドに移ってから数秒で、その変更を読み取るエフェクトが実行される前にアプリを中断することがあります。ネイティブアプリの内部に表示されるアプリは、そのアプリの状態に従います。アプリが複数のウィンドウを持つ場合(iPad やデスクトップ)、ビューはそれが属するウィンドウに従います。テストでは、nsh/test の app.setLifecycle("background") でアプリをその状態にできます(テスト)。

#セーフエリア

ノッチ、ホームインジケーター、ステータスバー、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)) など)。パディングは、インセットが変わるとネイティブに更新されます(セーフエリア)。
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 では、ツールバーの下に組み込まれたビューは、UIKit のセーフエリアと同様に、システムバーのうち自分と重なる部分だけを取得します。ステータスバーまで届かない場合、上のインセットはありません。

#プラットフォーム

Rust の os()、os_version()、idiom()、is_desktop()、is_catalyst() と、TSX の nsh からの platform 定数は、アプリが動作している環境を表します。アプリの実行中は変化しません。

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、またはデスクトップブラウザー(デスクトップとモバイル)。
is_catalyst()platform.catalystMac Catalyst(どちらのイディオムでも)。

TSX の platform の全メンバーは次のとおりです。

メンバー説明
platform.os"ios"、"android"、"macos"、"windows"、"linux"、または "web"。Mac Catalyst は "ios" を返します。
platform.version文字列。iOS とデスクトップ: OS のバージョン("17.5")、Android: API レベル("35")、Web: "0.0"。
platform.idiom"phone"、"pad"、"mac"、"tv"、または "desktop"。"desktop" は platform.desktop が true の場合に限ります。"mac" は Mac Catalyst における UIKit の Mac イディオムで、モバイルのイディオムです。
platform.desktopデスクトップの動作が適用されます(macOS、Windows、Linux、デスクトップブラウザー)。desktop={{…}} props と platform.select({ desktop }) はこれに従います。
platform.catalystアプリが Mac Catalyst(os "ios")で、Mac または iPad のイディオムで動作しています。
platform.select(spec)このプラットフォーム向けの値。プラットフォーム固有のコードを参照してください。
use nsh_core::{element::Os, prelude::*};

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

nsh test は iOS としてヘッドレスで動作します。

Linux では、platform.os は "linux"、イディオムは "desktop" で、platform.version はカーネルのメジャーバージョンとマイナーバージョン("6.12")です。ディストリビューションごとにリリースの番号付けが異なり、比較できないためです。この環境では、セーフエリアとキーボードの高さは 0 で、colorScheme() はデスクトップの設定(XDG settings portal)にリアルタイムで追従し、fontScale() はデスクトップのテキストスケーリング、pixelRatio() はディスプレイのスケール(小数を含む)に追従します。

macOS(AppKit バックエンド)では、platform.os は "macos"、イディオムは "desktop" で、platform.version は macOS のバージョンです。セーフエリアとキーボードの高さは 0 で、colorScheme() はシステムの外観にリアルタイムで追従し、fontScale() は 1、pixelRatio() はディスプレイのバッキングスケールです。

Windows では、platform.os は "windows"、イディオムは "desktop" で、platform.version は "10.0" です(Windows 11 でも同じです。Windows はバージョン番号を維持しています)。セーフエリアとキーボードの高さは 0 で、colorScheme() は「設定 > 個人用設定 > 色」にリアルタイムで追従し、fontScale() は「テキストのサイズ」設定(1 から 2.25)、reduceMotion() は「アニメーション効果」設定、pixelRatio() はモニターのスケールに追従し、Intl はユーザーの地域の形式でフォーマットします。

ブラウザー(Web ランタイム)では、platform.os は "web"、platform.version は "0.0" です。イディオムは、マウスがあり、ウィンドウの幅が 600 ポイント以上の場合は "desktop"、タブレットでは "pad"、スマートフォンでは "phone" となり、アプリの起動時に決まります。ロケール、reduceMotion()、fontScale()(ブラウザーの文字サイズ)、pixelRatio()、セーフエリア、キーボードの高さはブラウザーから取得され、リアルタイムで追従します。

#ピクセル密度

Rust の pixel_ratio() と round_to_nearest_pixel(points)、TSX の nsh からの pixelRatio() は、ポイントとデバイスピクセルを相互に変換します。

RustTSX説明
pixel_ratio()pixelRatio()ポイントあたりのデバイスピクセル数(ほとんどのスマートフォンでは 2 または 3)。リアクティブです。
round_to_nearest_pixel(points)Math.round(x * pixelRatio()) / pixelRatio()ポイントを最も近いデバイスピクセルに丸めた値。

デバイスピクセル 1 つ分の高さのヘアライン:

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

pixel_ratio() と pixelRatio() はリアクティブなので、ウィンドウを別の密度の画面に移動すると、ヘアラインもそれに追従します。

#右から左への表記

デバイスの言語が右から左に書かれる言語(アラビア語、ヘブライ語、ペルシア語など)の場合、NSH はアプリを右から左にレイアウトします。行は右から始まり、論理スタイルプロパティは右から左の側を採用し、テキストは右揃えになり、ネイティブウィジェットは反転します。レイアウトの方向は、ユーザーの言語に自動的に従います。ビューの direction スタイル("ltr"、"rtl"、または "inherit"。Rust では .direction(LayoutDirection::..))は、そのサブツリーの方向を設定します。ルートビューに指定すると、アプリ全体の方向を設定します。

変更はアプリを再起動せずにすぐに適用されます。このページのデモでは 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")は、サブツリーの方向を上書きします。たとえば、電話番号やメディアプレーヤーのコントロールを左から右のままにしたい場合に使います。rtl: と ltr: の Tailwind バリアントは、方向ごとに適用されます。

横スクロールビューとリストは、右から左のレイアウトでは右端から始まります。Android では、ネイティブウィジェットを反転させるために、ホストアプリがマニフェストで android:supportsRtl="true" を宣言する必要があります。

#関連項目