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

要素

Resizable

Resizable グループは、パネルを横並びまたは縦積みに配置し、隣り合う 2 つのパネルの境界ごとにハンドルを置きます。たとえば、ドキュメントの横のサイドバー、コンソールの上のエディター、詳細の横のリストなどです。ユーザーがハンドルをドラッグすると境界が動き、各パネルはアプリが許可するサイズの範囲内にとどまります。ResizablePanelGroup が動作を担います。具体的には、フレームごとにネイティブで実行されるドラッグ、空きスペースの分配、リサイズカーソル、指で操作するための広いタッチ領域です。見た目はアプリ側が決めます。ハンドルは線も色もグリップも持たない帯であり、そこに描かれるものはアプリのインジケーターです。パネルもアプリのビューです。

el-resizable.tsx
el-resizable.tsxLive
import { mount, signal, Text, View } from "nsh";
import { ResizablePanel, ResizablePanelGroup } from "nsh/base";
import { desktop, Log, Panel, Segmented, Split, ui, wide } from "./palette";

// The sizes are the app's: the group reports them when a drag ends, and the
// app passes them back as each panel's `size`.
function App() {
  const sidebar = signal(desktop ? 220 : 140);
  const consoleSize = signal(120);
  const showConsole = signal<"Shown" | "Hidden">("Shown");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  const round = (sizes: number[]) => sizes.map((s) => Math.round(s)).join(" · ");

  // The handles' look: a hairline that lights up under the pointer and while dragged.
  const handle = "bg-border group-hover:bg-muted-foreground group-active:bg-primary";

  const label = (title: string, detail: string) => (
    <View style={{ gap: 4 }}>
      <Text style={{ fontSize: 13, fontWeight: "600", color: ui().text }}>{title}</Text>
      <Text style={{ fontSize: 12, color: ui().muted }}>{detail}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>resizable</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Drag a line between two panels" : "Drag a line between two panels with your finger"}
      </Text>

      <Split
        grow
        width={640}
        inspector={
          <>
            <Panel title="sizes">
              <Text style={{ fontSize: 13, fontVariant: ["tabular-nums"], color: ui().text }}>
                {`sidebar ${Math.round(sidebar())} · console ${Math.round(consoleSize())}`}
              </Text>
            </Panel>
            <Segmented label="console" options={["Shown", "Hidden"] as const} value={showConsole()} onChange={(v) => showConsole.set(v)} />
            <Log title="onResize" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 360 : 300, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <ResizablePanelGroup
            axis="horizontal"
            handleClass={handle}
            handleAccessibilityLabel="Resize the sidebar"
            onResize={({ sizes }) => log(`row: ${round(sizes)}`)}
          >
            <ResizablePanel size={sidebar()} minSize={100} maxSize={320} onResize={({ size }) => sidebar.set(size)}>
              <View style={{ flex: 1, padding: 14, gap: 10, backgroundColor: ui().inset.backgroundColor }}>
                {label("Files", `${Math.round(sidebar())} pt, 100 to 320`)}
              </View>
            </ResizablePanel>
            <ResizablePanel minSize={120}>
              <ResizablePanelGroup
                axis="vertical"
                handleClass={handle}
                handleAccessibilityLabel="Resize the console"
                onResize={({ sizes }) => log(`column: ${round(sizes)}`)}
              >
                <ResizablePanel minSize={80}>
                  <View style={{ flex: 1, padding: 14 }}>{label("Editor", "Takes the room left")}</View>
                </ResizablePanel>
                <ResizablePanel
                  size={consoleSize()}
                  minSize={60}
                  fixed
                  visible={showConsole() === "Shown"}
                  onResize={({ size }) => consoleSize.set(size)}
                >
                  <View style={{ flex: 1, padding: 14 }}>{label("Console", "Fixed: keeps its height")}</View>
                </ResizablePanel>
              </ResizablePanelGroup>
            </ResizablePanel>
          </ResizablePanelGroup>
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{h_resizable, resizable_panel, theme};
use nsh_core::prelude::*;

fn workspace() -> impl IntoElement {
    h_resizable()
        .handle_style(|s| s.bg(move || theme().border()))
        .handle_accessibility_label("Resize the sidebar")
        .on_resize(|sizes: &Vec<Points>| log::info!("sizes: {sizes:?}"))
        .child(
            resizable_panel()
                .size(px(240.0))
                .size_range(px(160.0)..px(480.0))
                .p_4()
                .child(text("Files").text_color(move || theme().muted_foreground())),
        )
        .child(
            resizable_panel()
                .p_4()
                .child(text("Editor").text_color(move || theme().foreground())),
        )
}

ResizablePanelGroup と ResizablePanel は Rust の nsh-base クレートである nsh/base に含まれています。パネルはグループの直接の子です。Rust では、h_resizable() と v_resizable() が各軸のグループを作り(resizable_panel_group().axis(..) は軸を値として受け取ります)、resizable_panel() がパネルを作ります。それぞれにプロパティごとのセッター、イベントごとのリスナー、型付きのスタイルメソッド、そしてコンテンツ用の .child(..) があります。.size_range(min..max) は両方の制限を一度に設定します。

#サイズ

パネルの size は、そのパネルが希望するサイズで、minSize と maxSize の範囲内で決まります。サイズを持たないパネルは、コンテンツに関係なく残りのスペースを均等に分け合います。すべてのパネルにサイズがある場合は、余ったスペースがそれらのサイズに比例して分配されます。ユーザーがハンドルをドラッグしている間、グループは JS を介さずコア内でパネル間のスペースを移動します。

  • ハンドルの前にあるパネルを大きくすると、後ろにあるパネルから近い順にスペースを取ります。各パネルは minSize まで縮み、fixed のパネルは飛ばされます。
  • ハンドルの前にあるパネルを minSize より小さくすると、その前にあるパネルからスペースを取ります。
  • どのパネルも maxSize を超えて大きくならず、パネルの合計がグループを超えることもありません。

ドラッグが終了してサイズが変わっていた場合、グループは表示中のすべてのパネルのサイズをレイアウト順に報告します(グループの onResize)。また、サイズが変わった各パネルは自身のサイズを報告します(パネルの onResize)。ドラッグで動いたパネルは、アプリが新しい size を渡すまでユーザーが設定したサイズを保持します。報告されたサイズを size として渡し直すことで、アプリはグループの寿命を超えてサイズを保持できます。これにより、ストレージから復元したレイアウトは、ユーザーが最後に設定したサイズで開きます。報告が行われるのはドラッグとスクリーンリーダーの操作ステップのみです。ウィンドウのリサイズやパネルの表示・非表示では何も報告されません。

グループが拡大または縮小したとき(ウィンドウがリサイズされたとき)は、サイズを持つパネルはそのサイズを保ち、サイズを持たないパネルが差分を受け持ちます。グループがサイズの合計に対して小さすぎる場合は、fixed のパネル(サイドバーやインスペクターなど)を除いてパネルが縮み、スペースが戻ると元のサイズに戻ります。visible={false} のパネルはスペースを取らず、他のパネルがそのスペースを分け合います。再び表示されると、元のサイズで戻ります。

<ResizablePanelGroup axis="vertical" handleClass="bg-border">
  <ResizablePanel>
    <Editor />
  </ResizablePanel>
  <ResizablePanel size={180} minSize={120} fixed visible={consoleOpen()}>
    <Console />
  </ResizablePanel>
</ResizablePanelGroup>

#ハンドル

表示中の 2 つのパネルの境界ごとにハンドルがあります。ハンドルは、軸に対して 9 ポイントの太さの帯で、境界を中心に配置され、その軸のリサイズカーソルを持ちます。タッチスクリーンでは、タッチ領域が両側にさらに 10 ポイント広がるため、1 ポイントの線でも簡単につかめます。ハンドルは group です。そこに描かれるのはアプリのインジケーターで、handleClass / handleStyle でスタイルを設定します。大きさは軸方向に 1 ポイント、長さは境界の全長なので、bg-border だけでどちらの軸でもヘアラインになります。w-[3px] または h-[3px] で太くでき、group-hover: クラスと group-active: クラスは、帯の上のポインターと、押下やドラッグに追従します。線より凝った表現(点のグリップやアイコン)が必要な場合は、renderHandle がインジケーターの内容を描画します。ハンドルの index、axis、state が渡され、状態が変わったときに再描画されます。再描画はドラッグ中に数回程度で、移動のたびには行われません。アニメーションするグリップはハンドルのサムです(handleThumbClass / handleThumbStyle)。これは線の中央に置かれる 1 つのビューで、縦線では縦向きに描かれ、横線では 4 分の 1 回転して描かれるため、1 つのクラスで両方の軸に対応できます。このビューは group-hover: クラスと group-active: クラスで表示と遷移が行われます。ハンドルは、ポインターがどこへ動いても、ドラッグ全体を通して押された状態を保ちます。

<ResizablePanelGroup
  handleClass="w-[3px] items-center justify-center bg-border"
  renderHandle={(handle) => (
    <View class={handle.state === "dragging" ? "h-8 w-1 rounded-full bg-primary" : "h-6 w-1 rounded-full bg-muted-foreground"} />
  )}
>
  {/* panels */}
</ResizablePanelGroup>

disabled は、すべてのハンドルをその位置に固定します(ロックされたレイアウト)。

#プロパティ

#ResizablePanelGroup

プロパティRust型デフォルト説明
axish_resizable(), v_resizable(), .axis(..)"horizontal" | "vertical""horizontal"横並び(ハンドルを左右にドラッグ)または縦積み(上下にドラッグ)。
disabled.disabled(..)booleanfalseドラッグ不可。ハンドルはその位置にとどまります。
handleClass, handleStyle.handle_style(|s| ..)string, StyleProp<ViewStyle>なし各ハンドルのインジケーター。境界上のハンドルの帯の中に置かれ、特に指定がない限り、軸方向に 1 ポイント、長さは境界の全長です(w-[3px], h-[3px])。group-hover: と group-active: は帯に追従します。
handleThumbClass, handleThumbStyle.handle_thumb_class(..), .handle_thumb_style(|s| ..)string, StyleProp<ViewStyle>なし各ハンドルのサム。線の中央に置かれるグリップで、縦線では縦向きに描かれ、横線では 4 分の 1 回転して描かれるため、1 つのクラスで両方の軸に対応できます。ハンドルがホバー、押下、ドラッグされている間も同じビューのままなので、group-hover: クラスと group-active: クラスで遷移させます(h-11 w-[3px] rounded-full bg-muted-foreground opacity-0 scale-y-0 transition group-hover:opacity-40 group-hover:scale-y-[0.45] group-active:opacity-90 group-active:scale-y-100)。
renderHandle.render_handle(|handle| ..)(handle: ResizeHandleContext) => JSX.Elementなし各インジケーターの内容。ハンドルの state が変わると再描画されます。
handleAccessibilityLabel.handle_accessibility_label(..)stringなし各ハンドルについてスクリーンリーダーが読み上げる内容(何をリサイズするか)。スクリーンリーダーは 10 ポイントずつ調整します。
children.child(..)JSX.ElementなしResizablePanel。グループの直接の子です。
class, styleスタイルメソッドstring, StyleProp<ViewStyle>size-fullグループ自体。クラスで指定しない限り、親を満たします。

#ResizablePanel

プロパティRust型デフォルト説明
size.size(..)numberなしグループの軸方向に希望する長さ(ポイント単位)。minSize と maxSize の範囲内で決まり、新しい値を渡すとリサイズされます。指定しない場合は残りのスペースを分け合います。
minSize.min_size(..), .size_range(..)number100最小サイズ。
maxSize.max_size(..), .size_range(..)number制限なし最大サイズ。
fixed.fixed(..)booleanfalseグループがパネルの合計に対して小さすぎる場合でもサイズを保ちます。自身のハンドルをドラッグすればリサイズできます。size を受け取ります。
visible.visible(..)booleantrue非表示の場合、スペースを取らず、他のパネルがそのスペースを分け合います。
children.child(..)JSX.Elementなしパネルの内容。
class, styleスタイルメソッドstring, StyleProp<ViewStyle>なしパネル自体の表面。軸方向のサイズはグループが決めます。

#ResizeHandleContext

フィールド型説明
indexnumberindex 番目の表示中のパネルの後ろにある境界。
axis"horizontal" | "vertical"リサイズする軸。
state"idle" | "hovered" | "pressed" | "dragging"ポインターまたは指がハンドルとともに移動した距離。

#イベント

イベントRust説明
onResize (ResizablePanelGroup).on_resize(|sizes: &Vec<Points>| ..)ドラッグが終了してハンドルが動いた場合、またはスクリーンリーダーがハンドルを調整した場合に発生します。sizes は、表示中のすべてのパネルのサイズで、レイアウト順(左から右、上から下)に並びます。
onResize (ResizablePanel).on_resize(|size: &Points| ..)ドラッグが終了してこのパネルのサイズが変わった場合に発生します。size は新しいサイズで、パネルの size として渡し直します。

#パーツ

パーツ配置先スタイル状態
グループResizablePanelGroup の class, style
パネルグループResizablePanel の class, style
ハンドルのインジケーター境界上のハンドルの帯handleClass, handleStyle, renderHandleポインター下では group-hover:、押下中またはドラッグ中は group-active:、renderHandle では state
ハンドルのサムハンドルの線の中央handleThumbClass, handleThumbStylegroup-hover:, group-active:(ドラッグが終わるまで押された状態)

#アクセシビリティ

  • 各ハンドルは、スクリーンリーダー向けの調整可能な要素で、handleAccessibilityLabel がラベルになります。値は、ハンドルの前にあるパネルのサイズで、そのパネルの minSize と maxSize の範囲内です。
  • VoiceOver でハンドルを上下にスワイプするか、TalkBack の音量キーを使うと、ドラッグと同様にハンドルが 10 ポイント動きます。グループは onResize でそれを報告します。disabled のグループでは、ハンドルはその位置にとどまります。
  • パネルの内容はアプリのものであり、他の場所と同じように読み上げられます。

ラベル、ロール、アナウンスの全般については、アクセシビリティを参照してください。

#プラットフォームに関する注意

  • デスクトップ(macOS、Windows、Linux、GPUI)と Web: ハンドル上のポインターには列または行のリサイズカーソルが表示され、マウスまたはタッチパッドでドラッグすると境界が動きます。
  • iOS、Android、Mac Catalyst: 指でハンドルをドラッグします。タッチ領域は、帯より両側に 10 ポイントずつ広くなります。
  • 右から左へのレイアウトでは、パネルとそのサイズはレイアウトの順序に従います。
  • どのプラットフォームでも、アプリがスタイルを設定したもの(インジケーターとパネル)が描画されます。
  • Dock の分割は Resizable グループです。同じ方法でリサイズされます。