メインコンテンツまでスキップ

ウィンドウの合成

COMPOSITOR.window.composition は ShojiWM のカスタマイズの心臓部です。ウィンドウを 受け取り、それをどう配置・装飾するかを表す TSX ツリーを返す関数を割り当てます。 コンポジターはすべてのトップレベルウィンドウに対してこれを呼び、読み取った値が 変化するたびに(差分的に)再実行します。

COMPOSITOR.window.composition = (window) => (
<ManagedWindow rect={window.position} zIndex={1}>
<WindowBorder
style={{
borderRadius: 10,
border: {
px: 2,
color: window.isFocused((f) => (f ? "#d7ba7d" : "#4f5666")),
},
}}
>
<Box direction="column">
<Box
direction="row"
style={{ height: 28, paddingX: 8, gap: 8, alignItems: "center" }}
>
<AppIcon icon={window.icon} style={{ width: 16, height: 16 }} />
<Label text={window.title} style={{ flexGrow: 1, fontSize: 13 }} />
</Box>
<ClientWindow />
</Box>
</WindowBorder>
</ManagedWindow>
);

ツリーには必ず、ちょうど1つの <ManagedWindow/> が、ちょうど1つの <ClientWindow/> を包む形で含まれている必要があります。その間にある もの――ボーダー・タイトルバー・ボタン――があなたの装飾で、SSD コンポーネント で組み立てます。

window オブジェクト

引数は WaylandWindow、つまり1つのウィンドウへのライブでリアクティブなハンドルです。 合成内でそのシグナルを読むと、変更に自動的に購読されます。

リアクティブなプロパティ

それぞれ ReadonlySignal です――window.title()window.title.value のように 読むか、window.isFocused((f) => f ? 'a' : 'b') のようにマップします。

プロパティ意味
titlestringウィンドウタイトル
appIdstring | undefinedアプリケーション id(例: "org.gnome.Nautilus"
iconWindowIcon | undefinedアプリケーションアイコン
isFocusedbooleanキーボードフォーカスを持つ
isFloatingbooleanフローティング(非タイル)
isMaximizedboolean最大化
isFullscreenbooleanフルスクリーン
decorationWindowDecorationState有効な CSD/SSD ネゴシエーション状態
isResizablebooleanクライアントがインタラクティブリサイズを許可
isTransientboolean別ウィンドウの子(ダイアログ)
parentIdstring | undefinedトランジェントの場合の親ウィンドウ id
sizeConstraintsWindowSizeConstraintsクライアントの最小/最大サイズ
interactionスナップショット現在のポインター/ドラッグの状態

非リアクティブなヘルパー: id(安定した文字列)、position / rect(現在の論理 ジオメトリ)、state(ウィンドウごとのストア。状態とシグナル を参照)、transform(GPU トランスフォーム)、animationアニメーション を参照)。

クライアント側/サーバー側装飾

Wayland アプリは、自分でタイトルバーとボーダーを描く CSD(Client-Side Decoration)か、コンポジターに描画を任せる SSD(Server-Side Decoration)を 利用します。ShojiWM の方針を一度設定し、ネゴシエーション結果を composition で 読み取ります。

COMPOSITOR.window.decoration.configure((window, context) => {
const appId = (window.appId() ?? "").toLowerCase();

// アプリを識別できるまでは CSD の初期値を維持します。
if (appId.length === 0) {
return { mode: context.clientPreference ?? "client" };
}
if (appId.includes("firefox")) {
return { mode: "client" };
}
return { mode: "server" };
});

COMPOSITOR.window.composition = (window) => {
const decoration = window.decoration();
const useClientDecoration =
decoration.mode === "client" &&
!(
decoration.clientPreference === "server" &&
decoration.configuredMode === "server"
);

return (
<ManagedWindow rect={window.position} zIndex={1}>
{useClientDecoration ? (
<ClientWindow />
) : (
<WindowBorder style={{ border: { px: 2, color: "#4f5666" } }}>
<Box direction="column">
{/* タイトルバー */}
<ClientWindow />
</Box>
</WindowBorder>
)}
</ManagedWindow>
);
};

resolver は装飾オブジェクトの作成、クライアント要求の変更、関連メタデータの変更、 TS config のリロード時に同期的に実行されます。context の内容は次の通りです。

旧 KDE decoration manager のグローバル初期値は、ウィンドウ単位のメタデータより先に 送られるため、CSD に設定されています。最初の装飾要求時点では app id が空の場合があるので、 上の例のように app id が空の間は CSD の初期値を維持し、メタデータ到着後にアプリごとの 最終方針を適用してください。早い段階で SSD を返すと、Firefox や Chromium が簡素な ウィンドウ枠を構築し、後から CSD を返しても完全には作り直さないことがあります。resolver は 副作用なしである必要があり、この中で focus() などのウィンドウ操作を呼ぶとエラーになります。

プロパティ意味
protocolxdg-decoration-v1kde-server-decorationxwaylandnone のいずれか
clientPreferenceクライアントが要求したモード。未指定なら null
canNegotiate装飾プロトコルを通して決定をクライアントへ送れるか
reasonポリシーが評価された理由

window.decoration.configuredMode は ShojiWM が最後に選択したモードです。 window.decoration.mode はクライアントが ack し commit した有効なモードなので、描画の 通常の基準にはこちらを使います。XDG の configure/ack/commit 中は、両者が一時的に 異なる場合があります。clientPreferenceconfiguredMode がどちらも server なら、 クライアントとコンポジターはすでに SSD で合意しているため、上の例では effective mode の 反映を待たず SSD を描画します。これにより、activation まで configure の commit を遅らせる ツールキットでも起動直後に装飾なしの状態にならず、CSD を要求したクライアントへ SSD を 強制することもありません。

canNegotiatefalse の場合でも、ShojiWM はどちらの composition を描くか選べます。 ただし、クライアントに CSD の追加や削除を強制することはできません。XWayland は protocol: 'xwayland' として確認できますが、これらの Wayland 装飾プロトコルでは ネゴシエーションされません。

旧 KDE プロトコルでは、クライアントとコンポジターの再交渉ループを防ぐため、ShojiWM は 同じackの繰り返しを抑止します。同じ要求が8回到着すると、そのループについて一度だけ 警告を通常ログへ記録します。要求ごとに警告を出し続けることはありません。XDG decoration の要求には、プロトコルで必須の configure 応答を毎回返します。

メソッド

メソッド効果
close()クライアントに閉じるよう要求
maximize() / unmaximize()最大化の切り替え
minimize()最小化
fullscreen() / unfullscreen()フルスクリーンの切り替え
focus()キーボードフォーカスを与え前面に出す
scheduleAnimation(options)マネージドウィンドウのジオメトリをアニメーション
cancelAnimation(channel?)実行中のアニメーションをキャンセル
setCloseAnimationDuration(ms)閉じるアニメーションに合わせてサーフェス破棄を遅延
isXWayland()XWayland 上で動作中なら true

ManagedWindow

<ManagedWindow/> はウィンドウをレイアウトシステムに結びつけるアンカーです。 ウィンドウごとに1つ置きます。

Prop意味
rectManagedWindowRectウィンドウの論理的な {x, y, width, height}
zIndexnumber重なり順(大きいほど上)
workspacestring | numberワークスペース割り当て
visibleOutputsstring[] | null指定出力に限定(null で全出力)
visiblebooleanアンマップせずに表示/非表示
idlebooleanフォーカス巡回から除外。背景として扱う
interactivebooleanfalse のときポインター入力を無視
forceRectSizebooleanクライアントを rect のサイズに強制
tiledbooleanタイル状態をクライアントに送る
opacitynumber0.01.0
transformManagedWindowTransform追加の GPU トランスフォーム
allowTearingbooleanフルスクリーン+ダイレクトスキャンアウト時のテアリングを許可(ゲーム向け)

すべての prop はリアクティブなレイアウトのためにシグナルを受け付けます。rectzIndex などは通常、あなたのウィンドウマネージャのロジックが駆動します。

ClientWindow

<ClientWindow/> はクライアントの実際のサーフェスバッファを描画します。リーフノードで 子要素は持ちません。別名: <Window/>

<ClientWindow />

素の ClientWindow は、xdg_surface.window_geometry の外側にある CSD の影や透明な リサイズマージンを含め、クライアント所有のサーフェスツリー全体を保持します。 ManagedWindow のスロットを占有しているという理由だけでは切り取られません。

クリップを所有するのは周囲の SSD 階層です。border を持つコンテナで囲むと、子要素は そのボーダーの内側(角丸を含む)に切り取られます。明示的に切り取る場合は overflow: "hidden"、border があっても切り取りたくない場合は overflow: "visible" を指定します。

<WindowBorder
style={{ border: { px: 2, color: borderColor }, borderRadius: 8 }}
>
<ClientWindow />
</WindowBorder>
フルスクリーンのファストパス

フルスクリーンのウィンドウでは、<ManagedWindow/> の中に素の <ClientWindow/> だけを返します(ボーダーもタイトルバーもなし)。他に何も描画しないことで、TTY バックエンドがクライアントバッファをプライマリプレーンに昇格(ダイレクトスキャンアウト) でき、最小のレイテンシになります。デフォルト設定はまさにこれを行っています。