---
title: "UI ツリーとレイアウト — zenit Zig UI ドキュメント"
description: "ノードコンストラクタと Flex スタイルのレイアウトで、zenit の空間モデルを組み立てます。"
url: https://zenit.z.express/ja/docs/guide/ui-tree
language: ja
alternate_en: https://zenit.z.express/docs/guide/ui-tree.md
alternate_zh: https://zenit.z.express/zh/docs/guide/ui-tree.md
alternate_es: https://zenit.z.express/es/docs/guide/ui-tree.md
alternate_ko: https://zenit.z.express/ko/docs/guide/ui-tree.md
alternate_fr: https://zenit.z.express/fr/docs/guide/ui-tree.md
alternate_de: https://zenit.z.express/de/docs/guide/ui-tree.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# UI ツリーとレイアウト

ノードコンストラクタと Flex スタイルのレイアウトで、zenit の空間モデルを組み立てます。

## メンタルモデル

マウント関数は、実体を持ち常駐する `*ui.Node` のツリーを一度だけ構築します。コンテナは子の並べ方を決め、リーフはテキスト、画像、アイコンを持ちます。以降の各フレームでは、フレームワークは「ダーティ」なノードからだけパイプラインに再入します。レイアウト、描画項目の生成、Metal への受け渡しです。

FRAME PIPELINE

すべてのスタイルフィールドにはダーティレベルがあります。背景の変更は描画項目を再生成するだけですが、幅の変更は親の再レイアウトも引き起こします。ツリーがクリーンでアニメーションもなければ、そのフレームの GPU 送信全体がスキップされます。

## コンストラクタ

| コンストラクタ | 用途 |
| --- | --- |
| `ui.box` | 方向、サイズ、間隔、背景を完全に制御できる汎用コンテナ。既定は direction が column、幅と高さが fit |
| `ui.hstack / ui.vstack` | direction を row / column に固定した box |
| `ui.text / ui.textFmt` | 静的テキスト / Signal を購読する書式付きテキスト：`textFmt(cx, scope, fmt, .{signals}, props)` |
| `ui.icon / ui.iconTint / ui.svg / ui.image` | ベクターアイコン、着色アイコン、SVG、ビットマップテクスチャ |
| `ui.grid` | 列 / 行のトラックに沿って配置するグリッドコンテナ |
| `ui.spacer` | 幅と高さがともに grow の伸縮する空白 |
| `ui.clickable` | 任意のノードに `on_click` ハンドラを付け、同じノードを返す |
| `ui.boxStyled / hstackStyled / vstackStyled / textStyled` | 名前付きのスタイル関数を受け取り、テーマ変更時に自動で再適用。[スタイルとテーマ](https://zenit.z.express/ja/docs/guide/styling)を参照 |

> TIP
> 
> **まず既存のウィジェットを探しましょう。** ボタン、入力欄、リストなどのコントロールは `ui.widgets` に状態、フォーカス、キーボードの意味論を備えた形で用意されています。コンストラクタはそれらの間の構造を組むためのものです。[コンポーネントサイト](https://zenit.z.express/ja/components)をご覧ください。

## レイアウトの例

まずコンテナを作り、`appendChild` で子を追加します。スタイル値はまず `cx.tokens` から取るので、テーマの切り替えや間隔の一括調整でリテラルを探し回る必要はありません。

`card.zig`

```zig
const card = try ui.vstack(cx, .{
    .width = .fixed(360),
    .gap = cx.tokens.space._4,
    .padding = ui.Padding.all(cx.tokens.space._6),
    .background = cx.tokens.color.bg_secondary,
    .corner_radius = cx.tokens.radius.xl,
    .align_items = .stretch,
}, .{});

const header = try ui.hstack(cx, .{
    .gap = cx.tokens.space._2,
    .align_items = .center,
}, .{});

try header.appendChild(cx.allocator, try ui.iconTint(
    cx,
    ui.icons.star,
    cx.tokens.color.accent,
    .{ .width = .fixed(18), .height = .fixed(18) },
));
try header.appendChild(cx.allocator, try ui.text(cx, "Overview", .{}));
try header.appendChild(cx.allocator, try ui.spacer(cx));
try card.appendChild(cx.allocator, header);
```

子は構築時に tuple として渡すこともできます。`ui.Padding.symmetric(v, h)` は上下と左右のパディングを別々に設定します。

`row.zig`

```zig
// Children can also be passed as a tuple at construction time.
const row = try ui.hstack(cx, .{
    .width = .fill(),
    .padding = ui.Padding.symmetric(cx.tokens.space._2, cx.tokens.space._4),
    .gap = cx.tokens.space._3,
}, .{
    try ui.text(cx, "Name", .{}),
    try ui.spacer(cx),
    try ui.text(cx, "42", .{ .color = cx.tokens.color.fg_secondary }),
});

// Any node can take a click handler.
// (model: *Model from cx.bindState; Model.select is fn (*Model) void)
_ = ui.clickable(row, cx.on(Model, model, Model.select));
```

[Video](https://zenit.z.express/media/stories/stack.mp4?v=f9618b3adf)

実際の [Stack](https://zenit.z.express/ja/components/stack) Story：水平、垂直、入れ子のコンテナはすべて同じ rect、gap、align、sizing のルールで解決されます。

## サイズと座標

幅と高さは `ui.Sizing` ユニオンで、4 つのメンバーそれぞれに省略形があります。

| 省略形 | メンバー | 意味 |
| --- | --- | --- |
| `.fixed(120)` | `.{ .px = 120 }` | 固定ピクセル |
| `.fill()` | `.{ .grow = .{} }` | 残りの空間を占有。`grow` は `min` / `max` のペイロードを持つ（例：`.{ .grow = .{ .min = 200 } }`） |
| `.pct(50)` | `.{ .percent = 50 }` | 親の content box（padding を除く）に対する割合（0–100） |
| — | `.{ .fit = .{} }` | 内容によって決まる。box の既定値で、min / max も指定可能 |

SIZING

親のサイズが変わると、.fixed は動かず、.pct は content box に比例して追従し、.fill は固定サイズ、割合、gap を差し引いた残りをすべて取ります。

レイアウト結果はノードのフィールドには保存されません。`node.rectFromWorldOrFallback()` は直前のレイアウトで得られた、親の左上を基準とする矩形を返します。ウィンドウ座標が必要なら `node.globalRect()` を使います。これは祖先チェーンをたどって位置、translate、sticky オフセットを累積します。

```zig
// Parent-relative rect from the last layout pass.
const local = node.rectFromWorldOrFallback();

// Window coordinates: accumulates ancestors, translate and sticky offsets.
const screen = node.globalRect();
std.log.info("{d}x{d} at ({d}, {d})", .{ screen.w, screen.h, screen.x, screen.y });
_ = local;
```

[Video](https://zenit.z.express/media/stories/layoutbox.mp4?v=4d3c4ba31b)

LayoutBox Story：ボックスモデル、割合、絶対配置、Flex / Grid、クリッピング。各グループに padding box と content box のガイド線付き。

> TIP
> 
> **まず親ノードを確認しましょう。** 揃えやクリック領域がおかしいときは、原因はたいていリーフではなく親の padding、gap、direction、sizing にあります。開発中は `ui.devtools.overlay.attach(cx, scope, root, .{})` でホバーインスペクタを有効にすると、各ノードの矩形を直接確認できます。[DevTools](https://zenit.z.express/ja/docs/advanced/devtools) を参照してください。

## ノードの更新

ノードを直接変更するときは、パイプラインのどの部分を再実行すべきかをフレームワークに伝える必要があります。`setStyle` を優先してください。フィールドからコンパイル時にダーティレベルを選ぶので、手動の `markLayoutDirty` / `markRenderDirty` は不要です。

| レベル | 代表的なフィールド | 次のフレーム |
| --- | --- | --- |
| `.sizing` | `width, height` | 自身と親を再レイアウト |
| `.layout` | `padding, margin, gap, direction, justify, align_items, min/max_*` | このノードを再レイアウト |
| `.interaction` | `opacity, translate_*, corner_radius, border, z_index, cursor` | ヒットインデックスを更新して再描画 |
| `.render` | `background, shadow, gradient, outline, text_color` | 描画項目のみ再生成 |
| `.none` | `tab_index, layout_isolation` | フレーム処理なし |

`update.zig`

```zig
// setStyle picks the dirty level from the field at compile time.
node.setStyle(cx.allocator, .width, .fixed(240)); // sizing
node.setStyle(cx.allocator, .gap, 12);             // layout
node.setStyle(cx.allocator, .background, next);    // render

// Low-frequency fields live in StyleExt and need a real allocator.
node.setStyle(cx.allocator, .z_index, 10);

// Replace text: dupes the content, frees the previous owned copy,
// and re-measures only if the content actually changed.
try node.setTextContent(cx.allocator, "Updated");

// Hide without unmounting: display:none leaves layout, paint,
// hit-testing, Tab order and the a11y tree; state stays alive.
panel.setDisplay(.none);
panel.setDisplay(.flex); // back, same nodes

// Recolor an icon node whether it is icon-table or image (svgTint) backed.
_ = icon.setTint(cx.tokens.color.accent); // false: node has neither
```

`setText` は新旧テキストのシグネチャを比較します。計測に影響すれば sizing、見た目だけなら render をダーティにし、完全に同じなら何もしません。したがってテキスト更新後に `markRenderDirty` を呼ぶ必要はありません。

サブツリーを一時的に隠すには `node.setDisplay(.none)` を呼びます（または BoxStyle に `.display = .none` を書きます）。ノードとそのサブツリーは領域を占めず、gap にも数えられず、描画もされず、ヒットテスト、Tab 移動、アクセシビリティツリーからも外れます。ノードと状態は生きたままで、`.flex` で元に戻せるため、アンマウントして再構築する必要はありません。`setDisplay` は自身と親のレイアウトを自動でダーティにします。opacity を 0 にするだけでもヒットテストは止まりますが、領域は占めたままで Tab 順にも残ります。

> WARNING
> 
> **テキストの置き換えには setTextContent を。** `getText()` してから `content` を一時的な文字列に向け、`setText` で戻すのはやめてください。`setText` は内容をコピーせず、古い内容が owned だった場合は `owned` フラグもそのままコピーされ、新しい slice の所有権と混同されます。`setTextContent` は新しい内容を複製して owned とし、以前のコピーの解放はフレームワークに任せます。`TextProps` を自分で組み立てるときは `props.setContent(cx.allocator, src)` を使います。16 バイト以下はインラインバッファに入りアロケーションなし、それより長い内容は owned として複製されます。`setInlineContent` は 16 バイトを超えると黙って切り詰めず `error.InlineContentTooLong` を返します。

> NOTE
> 
> **低頻度のフィールドには allocator が必要です。** `shadow`、`z_index`、`corner_radius`、`min_width` などのフィールドは、必要に応じて確保される StyleExt に格納されます。これらにリテラルの `null` allocator を渡すとコンパイルエラーになります。迷ったら `cx.allocator` を渡してください。
