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

ドキュメント

レイアウト

NSH はすべての要素を、CSS の flexbox、grid、ブロックレイアウトの Rust 実装である Taffy でレイアウトします。デフォルトでは、子要素は縦に積まれ、縮小しないように設定されています。レイアウトはコア内で、コードの各ターンの後に実行され、変更されたフレームだけがネイティブビューに送られます。テキストはプラットフォームのテキストエンジンによって同じスレッド上で計測されるため、レイアウトがメインスレッドを待つことはありません。

guide-layout.tsx
guide-layout.tsxLive
import { mount, Scroll, signal, Text, View } from "nsh";
import { button, buttonText, desktop, Inspector, tint, ui, wide } from "./palette";

type Direction = "column" | "row";
type Justify = "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly";
type Align = "stretch" | "flex-start" | "center" | "flex-end";

function Choice<T extends string>(props: { label: string; options: readonly T[]; value: T; onChange: (v: T) => void }) {
  return (
    <View style={{ gap: 4 }}>
      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>{props.label}</Text>
      <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6 }}>
        <Each of={props.options}>
          {(o) => (
            <View onPress={() => props.onChange(o())} {...button("secondary", { size: "sm", on: () => props.value === o() })}>
              <Text style={buttonText("secondary", { on: () => props.value === o() })}>{o()}</Text>
            </View>
          )}
        </Each>
      </View>
    </View>
  );
}

function App() {
  const direction = signal<Direction>("row");
  const justify = signal<Justify>("flex-start");
  const align = signal<Align>("stretch");
  const colors = () => [ui().down, ui().info, ui().up];

  const box = (height: number) => (
    <View
      style={{
        height,
        padding: 10,
        gap: 8,
        borderRadius: 12,
        ...ui().inset,
        flexDirection: direction(),
        justifyContent: justify(),
        alignItems: align(),
      }}
    >
      <Each of={colors()}>
        {(c, i) => (
          <View style={{ minWidth: 44, minHeight: 44, padding: 6, borderRadius: 8, backgroundColor: c() }}>
            <Text style={{ color: ui().page, fontWeight: "700" }}>{i() + 1}</Text>
          </View>
        )}
      </Each>
    </View>
  );
  const more = () => (
    <>
      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>flex: 1 / 2 / fixed, percentages, aspectRatio</Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <View style={{ flex: 1, height: 36, borderRadius: 6, backgroundColor: tint(ui().info, 0.3) }} />
        <View style={{ flex: 2, height: 36, borderRadius: 6, backgroundColor: tint(ui().info, 0.6) }} />
        <View style={{ width: 36, height: 36, borderRadius: 6, backgroundColor: ui().info }} />
      </View>
      <View style={{ flexDirection: "row", gap: 8, alignItems: "flex-start" }}>
        <View style={{ width: "30%", aspectRatio: 1, borderRadius: 8, backgroundColor: tint(ui().warn, 0.35) }} />
        <View style={{ width: "40%", aspectRatio: 16 / 9, borderRadius: 8, backgroundColor: ui().warn }} />
      </View>

      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>grid</Text>
      <View style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6 }}>
        <View style={{ gridColumn: "span 2", height: 40, borderRadius: 6, backgroundColor: tint(ui().violet, 0.3) }} />
        <View style={{ height: 40, borderRadius: 6, backgroundColor: tint(ui().violet, 0.6) }} />
        <View style={{ gridRow: "span 2", borderRadius: 6, backgroundColor: ui().violet }} />
        <View style={{ gridColumn: "1 / 4", height: 40, borderRadius: 6, backgroundColor: tint(ui().violet, 0.8) }} />
      </View>

      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>position: absolute</Text>
      <View style={{ ...ui().inset, height: 100, borderRadius: 12 }}>
        <View style={{ position: "absolute", top: 8, left: 8, width: 60, height: 40, borderRadius: 6, backgroundColor: ui().up }} />
        <View style={{ position: "absolute", right: 8, bottom: 8, width: "40%", height: 30, borderRadius: 6, backgroundColor: ui().down }} />
        <View style={{ position: "absolute", top: 0, bottom: 0, left: "50%", width: 2, backgroundColor: ui().border }} />
      </View>
    </>
  );
  const justifyChoice = () => (
    <Choice
      label="justifyContent"
      options={["flex-start", "center", "flex-end", "space-between", "space-evenly"] as const}
      value={justify()}
      onChange={(v) => justify.set(v)}
    />
  );
  const alignChoice = () => (
    <Choice label="alignItems" options={["stretch", "flex-start", "center", "flex-end"] as const} value={align()} onChange={(v) => align.set(v)} />
  );

  return (
    <>
      {() =>
        wide() ? (
          <View style={{ flex: 1, backgroundColor: ui().page, flexDirection: "row", padding: 20, paddingTop: 24, gap: 16 }}>
            <Scroll style={{ flex: 1, minWidth: 0 }}>
              <View style={{ gap: 14 }}>
                {box(260)}
                {more()}
              </View>
            </Scroll>
            <Inspector>
              <Choice label="flexDirection" options={["row", "column"] as const} value={direction()} onChange={(v) => direction.set(v)} />
              {justifyChoice()}
              {alignChoice()}
            </Inspector>
          </View>
        ) : (
          <Scroll style={{ flex: 1, backgroundColor: ui().page }}>
            <View style={{ padding: 20, paddingTop: 24, gap: 14 }}>
              <View
                onPress={() => direction.set(direction() === "row" ? "column" : "row")}
                {...button("primary", { align: "flex-start" })}
              >
                <Text style={buttonText("primary")}>
                  flexDirection: {direction()} ({desktop ? "click" : "tap"} to switch)
                </Text>
              </View>
              {justifyChoice()}
              {alignChoice()}
              {box(220)}
              {more()}
            </View>
          </Scroll>
        )
      }
    </>
  );
}

mount(App);

以下のレイアウトプロパティはスタイルプロパティです。Rust では型付きのスタイルメソッド(.flex_row()、.gap_4()、.grid_cols(3)、.absolute().inset_0())、TSX では style prop のキーであり、それぞれに Tailwind クラス(flex-row、gap-4、grid-cols-3、absolute inset-0 など)があります。どちらも同じネイティブスタイルに変換されます。

#Flexbox

ビューは flexbox で子要素をレイアウトします。display を設定していないビューは、子要素を列に積みます。スタイルの display: "flex" は、flexDirection が設定されていない限り行になります。これは CSS や Tailwind の flex クラスと同じです。何も設定されていない場合は、次のデフォルトが適用されます。

プロパティNSH のデフォルトWeb のデフォルト
flexDirection"column"(display を設定していないビュー)、display: "flex" を設定すると "row"row
flexShrink01
alignContent"flex-start"(grid では "normal")normal
alignItems"stretch"stretch
minWidth / minHeight0auto
position"relative"static
view()
    .flex_row()
    .items_center()
    .gap_3()
    .p_4()
    .child(image(avatar).size(px(40.0)).rounded(px(20.0)))
    .child(
        view()
            .flex_1()
            .gap_0p5()
            .child(text(name).font_weight(FontWeight::SEMIBOLD))
            .child(text(message).text_color(rgb(0x6e7781)).number_of_lines(1)),
    )
    .child(text(time).text_color(rgb(0x6e7781)))
プロパティ値Rust
flexDirection"column"、"row"、"column-reverse"、"row-reverse".flex_col()、.flex_row()、.flex_col_reverse()、.flex_row_reverse()
justifyContent主軸:"flex-start"、"flex-end"、"start"、"end"、"center"、"space-between"、"space-around"、"space-evenly"、"stretch".justify_start()、.justify_center()、.justify_end()、.justify_between()、.justify_around()、.justify_evenly()
alignItems交差軸:"stretch"、"flex-start"、"flex-end"、"start"、"end"、"self-start"、"self-end"、"center"、"baseline".items_stretch()、.items_start()、.items_center()、.items_end()、.items_baseline()
alignSelf個々の子要素の交差軸方向の配置。"auto" は親の alignItems に従う.self_start()、.self_center()、.self_end()、.self_stretch()
alignContent折り返すコンテナの各行
flexWrap"nowrap"、"wrap"、"wrap-reverse".flex_wrap()
flex数値 1 つの省略記法(後述).flex_1()、.flex_auto()、.flex_none()
flexGrow、flexShrink、flexBasisCSS と同じ.flex_grow(n)、.flex_shrink(n)、.flex_shrink_0()、.basis(length)
gap、rowGap、columnGap子要素間のスペース.gap(length)、.gap_y(length)、.gap_x(length)、およびステップ .gap_2()、.gap_x_4() など

flex-start と flex-end はフレックス方向に従い、start と end は CSS と同様に書字方向に従います(右から左の場合、start は右側です)。self-start / self-end は子要素自身のものに従います。

flex は CSS の省略記法であり、Tailwind の flex-* クラスも同様です。数値 n は、n で拡大し、1 で縮小し、basis 0 から開始することを意味します。そのため、flex: 1 を指定した兄弟要素(Rust では .flex_1()、flex-1)は、コンテンツに関係なくスペースを均等に分け合います。"none" は 0 0 auto(ビューは自身のサイズを保つ)、"auto" は 1 1 auto、"initial" は 0 1 auto であり、grow、shrink、basis は個別に書くこともできます("2 0 50%"、"1 1 120px")。最小サイズのデフォルトは 0 なので、minWidth: "auto" でコンテンツのサイズを保たない限り、flex: 0(0 1 0)はビューを 0 まで縮小します。

view()
    .flex_row()
    .gap_2()
    // One share.
    .child(view().flex_1().h(px(36.0)).bg(rgb(0xc8e1ff)))
    // Two shares.
    .child(view().flex_grow(2.0).flex_shrink(1.0).basis(px(0.0)).h(px(36.0)).bg(rgb(0x79b8ff)))
    // Fixed.
    .child(view().size(px(36.0)).bg(rgb(0x0366d6)))

最小サイズのデフォルトが 0 であるため、行の中の flex: 1 テキストは、兄弟要素を押し出すことなく縮小して折り返されます。minWidth: "auto" / minHeight: "auto"(Rust では min-w-auto / min-h-auto、.min_w(auto()))は CSS の自動最小値です。縮小する要素は、Web と同様に、コンテンツの最小サイズ(テキストでは最も長い単語)で止まります。

#サイズ

プロパティ値Rust
width、heightポイント、親に対する割合("50%")、または "auto".w(px(120.0))、.h(relative(0.5))、.w_full()、.size(px(44.0))、ステップ .w_10() など
minWidth、maxWidth、minHeight、maxHeightポイントまたは割合。minWidth / minHeight では、コンテンツの最小サイズを表す "auto" も使えます.min_w(..)、.max_w(..)、.min_h(..)、.max_h(..)
aspectRatio幅を高さで割った値:1、16 / 9.aspect_ratio(16.0 / 9.0)、.aspect_square()
padding、paddingHorizontal、paddingVertical、paddingTop などポイントまたは割合.p(..)、.px(..)、.py(..)、.pt(..) など、およびステップ .p_4()、.px_2() など
margin、marginHorizontal、marginVertical、marginTop などポイント、割合、または "auto".m(..)、.mx(..)、.my(..)、.mt(..) など、.mx(auto())

Rust では長さに型があります。px(16.0) は 16 ポイント、relative(0.5) は親の 50%、auto() は auto です。ステップメソッドは、1 ステップあたり 4 ポイントという Tailwind のスペーシングスケールに従います(.p_4() は 16 ポイント、.gap_0p5() は 2 ポイント)。

サイズを指定しない場合、ビューはコンテンツ(とパディング)と同じ大きさになり、デフォルトの alignItems: "stretch" によって親の交差軸方向に引き伸ばされます。割合は親のサイズが基準です(padding と margin は、CSS と同様に親の幅が基準です)。

aspectRatio は、わかっている一方の寸法から、もう一方の寸法を決めます。

view().w_full().aspect_ratio(16.0 / 9.0).bg(black())

アプリのアセットの画像には固有のサイズがあり、サイズを指定しなければそのサイズでレイアウトされます(画像とアセット)。リモート画像は読み込まれるまでサイズを持たないため、サイズを指定してください。

#Grid

display: "grid"(Rust では .grid())は、子要素を CSS grid のトラックにレイアウトします。

view()
    .grid()
    .grid_template_columns("repeat(3, 1fr)")
    .gap_2()
    .child(view().col_span(2).h(px(60.0)).bg(rgb(0xd8b9ff)))
    .child(view().h(px(60.0)).bg(rgb(0xb083f0)))
    .child(view().row_span(2).bg(rgb(0x8250df)))
    .child(view().col_start(2).col_end(4).h(px(60.0)).bg(rgb(0x6639ba)))
プロパティ値Rust
gridTemplateColumns、gridTemplateRowsCSS のトラックリスト:"repeat(3, 1fr)"、"200px 1fr"、minmax()、auto-fill / auto-fit、ライン名.grid_cols(3)、.grid_rows(2)、.grid_template_columns("200px 1fr")、.grid_template_rows(..)
gridAutoColumns、gridAutoRows暗黙的に作成される列と行のトラック
gridAutoFlow"row"、"column"、"dense"、"row dense"、"column dense".grid_flow_col()、.grid_flow_dense()
gridColumn、gridRow"1 / 3"、"span 2"、ライン番号または名前:"content-start / content-end"、"2 col"(col という名前の 2 番目のライン)、"span col"(col という名前の次のラインまで).col_span(2)、.row_span(2)、.col_span_full()
gridColumnStart、gridColumnEnd、gridRowStart、gridRowEndライン番号または名前.col_start(1)、.col_end(3)、.row_start(..)、.row_end(..)(ライン番号)
justifyItems、justifySelfアイテムが行方向にエリア内でどのように配置されるか:"start"、"end"、"center"、"stretch" など
alignItems、alignSelfアイテムが列方向にエリア内でどのように配置されるか.items_*()、.self_*()
justifyContent、alignContentトラックが行方向と列方向に grid 内でどのように配置されるか.justify_*()

テンプレートでは、ラインに角括弧で 1 つまたは複数の名前を付け、CSS と同様にその名前でアイテムを配置します(Rust ではラインの番号で配置します)。

view()
    .grid()
    .grid_template_columns("[full-start] 16px [content-start] 1fr [content-end] 16px [full-end]")
    // content-start / content-end: lines 2 to 3.
    .child(view().col_start(2).col_end(3).h(px(60.0)).bg(rgb(0xb083f0)))
    // full-start / full-end: lines 1 to 4.
    .child(view().col_start(1).col_end(4).h(px(60.0)).bg(rgb(0x8250df)))

grid では、justifyContent と alignContent のデフォルトは CSS の "normal" です。サイズが auto のトラックはコンテナを埋めるように伸びるため、高さがあって行テンプレートのない grid では、その高さが各行で分け合われます。明示的に設定した値("start"、"center"、"space-between" など)は、CSS と同じようにトラックを配置します。アライメントクラスは、Tailwind CSS とまったく同じ値を設定します。content-start(align-content: flex-start)と place-content-start はトラックを先頭に詰め、content-normal と justify-normal はブラウザと同様にトラックを引き伸ばします。

display: "block" は CSS のブロックレイアウトを使用し(子要素は積み重ねられ、それぞれコンテナと同じ幅になります)、display: "none"(.hidden())はビューをレイアウトから除外します。

#絶対配置

position: "absolute"(.absolute())はビューをフローから外し、親を基準に top、right、bottom、left で配置します。

view()
    .h(px(200.0))
    .child(image(cover).absolute().inset_0())
    .child(
        view()
            .absolute()
            .left_3()
            .right_3()
            .bottom_3()
            .child(text("Caption").text_color(white()).font_weight(FontWeight::BOLD)),
    )

オフセットにはポイントと割合を指定できます(Rust では .top(..)、.right(..)、.bottom(..)、.left(..)、.inset(..))。left と right(または top と bottom)を指定し、サイズを指定していないビューは、それらの間に引き伸ばされます。後の兄弟要素は前の要素より上に描画されます。zIndex(.z_index(n))で順序を変更できます。

#計測

onLayout(.on_layout(..))は、変更があったレイアウトのたびに、親を基準としたビューのフレームを通知します。

let width = signal(0.0_f32);

view()
    .on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))
    .child(text(move || format!("{} points wide", width.get())))

フレームの単位はポイントです。Rust では Bounds<Points>、TSX では { bounds }、すなわち Bounds({ origin: { x, y }, size: { width, height } })です。ウィンドウ全体については、Rust では window()、TSX では nsh の windowSize() を読み取ります。どちらも回転やウィンドウのリサイズに追従し、これらを読み取るスタイルはウィンドウに合わせて変化します。

view().style_when(|| window().width > px(600.0), |s| s.flex_row())

Tailwind のブレークポイント(sm:、md: など)と portrait: / landscape: を使えば、コードを書かずに同じ結果が得られます。

#セーフエリア

ルートビューは、ステータスバーとホームインジケーターの下を含め、画面全体を覆います。画面には、CSS の env()(paddingTop: "env(safe-area-inset-top)"、Tailwind では pt-[env(safe-area-inset-top)]、Rust では .p_safe_area()。セーフエリアを参照)でセーフエリアのインセット分のパディングを付けるか、Rust では safe_area()、TSX では safeArea() / safeAreaInsets() でインセットを読み取って自分で適用します。ナビゲーションスタックのページは、すでにヘッダーの下に配置されています。

#右から左

アプリのレイアウト方向はシステムの言語に従います。ビューの direction スタイル("ltr"、"rtl"、または "inherit"。Rust では .direction(LayoutDirection::Rtl))は、そのサブツリーの方向を設定します。アプリが右から左にレイアウトされる場合は、次のようになります。

  • 行は右から始まります。
  • left と right は、ブラウザと同様に、スタイルキーでもクラスでも、それぞれの側を保ちます。paddingLeft、marginLeft、left、borderLeftWidth、borderTopLeftRadius、textAlign: "left" と pl-4、ml-2、left-0、border-l、rounded-tl-lg、text-left は左側のままです。
  • 論理プロパティは方向に従い、inline start は右側になります:marginStart、marginEnd、paddingStart、paddingEnd、start、end、borderStartWidth、borderEndColor、borderTopStartRadius など、textAlign: "start" / "end"(クラスでは ps-4、ms-2、start-0、border-s、rounded-ss-lg、text-start。Rust では .ms(..)、.me(..)、.ps(..)、.pe(..)、.border_s(..)、.rounded_ss(..) など)。marginHorizontal と paddingHorizontal は両側を設定します。
  • textAlign: "auto" を指定したテキストは右揃えになり、ネイティブコントロールは反転します。
view()
    .flex_row()
    .gap_2()
    .child(view().ms_4().rounded_ss(px(12.0)).size(px(44.0)).bg(rgb(0x54aeff)))

let rtl = signal(false);
view()
    .direction(move || if rtl.get() { LayoutDirection::Rtl } else { LayoutDirection::Ltr })
    .on_press(move |_: &()| rtl.set(!rtl.get()))
    .child(text(move || if rtl.get() { "Right to left" } else { "Left to right" }))

言語またはビューの方向を変更すると、再起動することなくただちに反映されます。direction スタイルを使うと、1 つのビューとその子要素を独自の方向に保つこともできます。たとえば、電話番号を左から右に表示する場合です。Android では、NSH を組み込むアプリで、ネイティブコントロールを反転させるために、マニフェストに android:supportsRtl="true" が必要です。

#関連項目