Murasaki
ガイド

コンテキストメニュー

useContextMenu で本物の OS コンテキストメニューを宣言する — HTML ポップアップは一切関与しません。

Murasaki のコンテキストメニューはマークアップではなくフックとして宣言するため、状態のすぐそばに書けます — action の中で useState のセッターをクロージャとして扱えます。HTML は一切レンダーされません。項目は Rust 側に投げられ、本物の OS メニュー(macOS では NSMenu、Windows では HMENU)がポップアップします。宣言する各レンダラーに menu:context を付与します。

murasaki.config.ts
window: { capabilities: ['menu:context', 'clipboard:writeText'] }

ネイティブロールには、それぞれ対応するケイパビリティも必要です。この例では <Action.Copy /> のために clipboard:writeText を付与しています。単純なカスタムコールバックであれば menu:context だけで十分です。

ウィンドウ全体のメニュー

id を指定せずに useContextMenu を呼び出すと、デフォルトメニューが宣言されます — スコープ付きメニューに占有されていない場所であれば、ユーザーがどこを右クリックしても開きます:

src/app/layout.tsx
import { useContextMenu, Action } from 'murasaki'

useContextMenu([
  { label: 'Reload', shortcut: 'command,R', action: <Action.Reload /> },
  { separator: true },
  { label: 'Copy', action: <Action.Copy /> },
])

スコープ付きメニュー

メニューに id を与え、対応する <ContextMenuTrigger> で領域にタグ付けします — その領域だけがそのメニューを開き、ウィンドウのデフォルトメニューより優先されます:

import { useContextMenu, ContextMenuTrigger } from 'murasaki'
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  useContextMenu('card', [
    { label: 'Increment', action: () => setCount((n) => n + 1) },
  ])

  return (
    <ContextMenuTrigger id="card">
      <div>Clicked {count} times</div>
    </ContextMenuTrigger>
  )
}

<ContextMenuTrigger> は、子要素がちょうど1つの場合はデフォルトでそれをクローンします(ラッパーノードなし)。複数の子要素がある場合など、代わりに display: contents<span> ラッパーを強制するには asChild={false} を渡します。

項目の形

各エントリはディバイダーか、以下のいずれかです:

interface ContextMenuEntry {
  label: string
  shortcut?: string          // 例: "command,I" — keydown にも直接紐づけられる
  disabled?: boolean
  action?: ContextMenuAction // 組み込みの <Action.*/> 要素、または関数
  items?: ContextMenuItemSpec[] // action の代わりのサブメニュー
}

type ContextMenuItemSpec = ContextMenuEntry | { separator: true }

shortcut は、メニューのネイティブなアクセラレーターラベルを設定すると同時に keydown ハンドラを登録するため、メニューが開いていなくても発火します。

Actions

action には、あなた自身の関数か、組み込みの <Action.*/> 要素のいずれかを指定できます — ネイティブ OS のロール(CopyPasteCutSelectAllUndoRedoQuit)、またはクライアントの挙動(ReloadNavigateRun)です。全リストはNative APIsを参照してください。

ネイティブのコンテキストメニューには、常に menu:context が必要です。ネイティブロールのアクションには、対応するネイティブ API を直接呼ぶ場合と同じ許可も必要です。

アクション追加で必要なケイパビリティ
Copy, Cutclipboard:writeText
Pasteclipboard:readText
Quitapp:quit
SelectAll, Undo, Redomenu:context 以外は不要

必要なロールのケイパビリティが欠けている場合、Murasaki はメニューのペイロード全体を拒否します。クライアント側の挙動や自作のコールバックには、追加のネイティブケイパビリティは不要です。

Linux では Copy/Cut/Paste/SelectAll は X11 キーイベントの合成によって実現されます(Wayland では XWayland が必要)。一方 Undo/Redo はメニューから静かに省かれます — muda/GTK 側の既知の制約です。Linux でこれらが必要な場合は、独自の action 関数を指定してください。

createActions による再利用可能な Actions

アプリのアクションを一度だけ定義すれば(典型的には src/lib/action.ts に、ストアに裏打ちされた形で)、どこからでも呼び出せます — そして <Action.name /> として、どのメニューにも組み込めます:

src/lib/action.ts
import { createActions } from 'murasaki'
import { useCounter } from './counter'

export const Action = createActions({
  increment: () => useCounter.getState().increment(),
  reset: () => useCounter.getState().reset(),
})
import { Action } from '@/lib/action'

useContextMenu('card', [
  { label: 'Increment', shortcut: 'command,I', action: <Action.increment /> },
  { label: 'Reset counter', action: <Action.reset /> },
])

createActions が返すオブジェクトには組み込みのアクション(Action.CopyAction.Reload など)も含まれるため、1つのファイルで Action をインポートするだけですべてをまかなえます。

ウィンドウ全体のメニュー(id なしの useContextMenu(items))は、同時に1つだけマウントされるべきです — 2つ目をマウントすると開発時に警告がログに出力され、最後にマウントされたものが優先されます。

次へ

GitHub でこのページを改善

On this page