decorations: false で標準のタイトルバーを消して自作のバーにすると(フレームレスウィンドウ)、メニューもその中に並べたくなります。ところが Tauri のメニューバー(menu-001)はページの外側に OS が描くもので、HTML の自作バーの中には置けません。そこで自作バーに「ファイル」「編集」のボタンを置き、押されたら Tauri のメニューをボタンの真下にポップアップで開きます。バーはページの一部のまま、開くメニューは OS 標準のものになり、コピーや貼り付けなどの標準項目もそのまま使えます。ドラッグで動かす領域と、最小化・閉じるなどのボタンは win-009 と win-006 で作ったものを使います。
前提条件
メニューの作成と表示の権限(core:menu:allow-new / core:menu:allow-popup)は core:menu:default に含まれ、それが core:default に含まれるので追加は要りません。移動や最小化などの権限は win-009 の表のとおり足します。下の例では、macOS で自作のメニューを隠す判定に OS プラグイン(npm run tauri add os)の platform() を使うので、os:default も入れています。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"os:default"
]
}
1. フロントエンドから実装する (TypeScript)
見出しのボタンを並べる
<button> は data-tauri-drag-region の付いた親の中に置いてもドラッグの対象から外れるので、そのまま押せます。
<header class="titlebar" data-tauri-drag-region>
<nav class="menubar">
<button type="button" data-menu="file">ファイル(F)</button>
<button type="button" data-menu="edit">編集(E)</button>
</nav>
<span class="titlebar-title" data-tauri-drag-region>My Editor</span>
<!-- 最小化・最大化・閉じるのボタンは win-009 と同じ -->
</header>
<textarea></textarea>
.titlebar { display: flex; align-items: center; height: 32px; user-select: none; }
.menubar button { height: 32px; padding: 0 10px; border: 0; background: transparent; font: inherit; }
.menubar button:hover { background: rgba(0, 0, 0, 0.08); }
.titlebar-title { flex: 1; text-align: center; }
body.native-menu .menubar { display: none; }
ボタンの真下にメニューを開く
メニューは起動時に 1 回だけ作り、開くたびに使い回します。押すたびに作り直すと、チェックや無効化の状態を保てません。見出しごとに Submenu を作っておけば、その popup() で中身だけを開けます。
popup() に位置を渡さないと、メニューはマウスの位置に開きます。ボタンの真下にそろえるには、getBoundingClientRect() の左端と下端を LogicalPosition で渡します。位置はウィンドウの左上が基準で、CSS の px は(ページをズームしていなければ)論理ピクセルと同じ単位です。PhysicalPosition に渡すと、拡大率 150% の画面ではずれます。
import { Menu, Submenu } from '@tauri-apps/api/menu';
import { LogicalPosition } from '@tauri-apps/api/dpi';
import { platform } from '@tauri-apps/plugin-os';
// 実際の処理は id で振り分ける(menu-005)
export const run = (id: string) => console.log('menu:', id);
const nativeMenu = platform() === 'macos'; // macOS は画面上部のメニューバーを使う
// 起動時に 1 回だけ作り、開くたびに使い回す
export const menus: Record<string, Submenu> = {
file: await Submenu.new({
text: 'ファイル',
items: [
{ id: 'file:new', text: '新規作成', accelerator: 'CmdOrCtrl+N', action: run },
{ id: 'file:open', text: '開く...', accelerator: 'CmdOrCtrl+O', action: run },
{ item: 'Separator' },
{ id: 'file:quit', text: '終了', action: run },
],
}),
edit: await Submenu.new({
text: '編集',
items: [
{ item: 'Cut', text: '切り取り' },
{ item: 'Copy', text: 'コピー' },
{ item: 'Paste', text: '貼り付け' },
{ item: 'Separator' },
{ item: 'SelectAll', text: 'すべて選択' },
],
}),
};
export async function openMenu(name: string) {
const menu = menus[name];
const button = document.querySelector<HTMLElement>(`[data-menu="${name}"]`);
if (!menu || !button) return;
const r = button.getBoundingClientRect();
await menu.popup(new LogicalPosition(r.left, r.bottom)); // ウィンドウ左上からの論理ピクセル
}
for (const button of document.querySelectorAll<HTMLButtonElement>('[data-menu]')) {
// 押してもフォーカスを入力欄に残す(編集メニューのコピーなどが効くように)
button.addEventListener('mousedown', (e) => e.preventDefault());
button.addEventListener('click', () => void openMenu(button.dataset.menu ?? ''));
}
// macOS は画面上部のメニューバーに置き、自作のボタンは隠す
if (nativeMenu) {
document.body.classList.add('native-menu');
const appMenu = await Submenu.new({ text: 'My Editor', items: [{ item: 'Quit' }] }); // 先頭はアプリ名の位置に入る
const bar = await Menu.new({ items: [appMenu, menus.file, menus.edit] });
await bar.setAsAppMenu();
}
ボタンの mousedown で既定の動作を止めているのは、押した瞬間にフォーカスが入力欄からボタンへ移るのを防ぐためです。移ってしまうと、編集メニューの「コピー」などが、それまで選択していた入力欄に効きません。標準項目の Undo / Redo は Windows と Linux では使えないので、元に戻す操作が要るなら自前の項目にしてエディターの処理を呼びます(menu-007)。
キー操作を足す
標準のメニューバーが無いので、Alt+F で「ファイル」を開く操作は自分で作ります。また、ショートカットキーが効くのはウィンドウやアプリのメニューとして付けたメニューで、ポップアップで開くだけのメニューの accelerator はキーの表示にとどまります。ショートカットはページの keydown で同じ処理を呼びます。
// (続き)
const altKeys: Record<string, string> = { KeyF: 'file', KeyE: 'edit' };
const ctrlKeys: Record<string, string> = { KeyN: 'file:new', KeyO: 'file:open' };
window.addEventListener('keydown', (e) => {
if (nativeMenu) return; // macOS はアプリのメニューとして付けたのでショートカットもそちらで効く
if (e.altKey && !e.ctrlKey && altKeys[e.code]) {
e.preventDefault();
void openMenu(altKeys[e.code]);
} else if (e.ctrlKey && !e.altKey && ctrlKeys[e.code]) {
e.preventDefault();
run(ctrlKeys[e.code]);
}
});
2. バックエンドから実装する (Rust)
メニューを Rust で持つと、項目の処理を on_menu_event にまとめられ、macOS ではそのままアプリのメニューに使えます。ページからは「どの見出しを、どこに開くか」だけを送り、Rust の popup_menu_at() で開きます。コマンド経由なので JS 側のメニューの権限は関係ありません。
use tauri::menu::{Submenu, SubmenuBuilder};
use tauri::{LogicalPosition, Manager, State, Window, Wry};
/// 自作バーの見出しごとのメニュー
struct TitlebarMenus {
file: Submenu<Wry>,
edit: Submenu<Wry>,
}
/// x, y はウィンドウ左上からの論理ピクセル(CSS の px をそのまま渡す)
#[tauri::command]
async fn open_titlebar_menu(
window: Window,
menus: State<'_, TitlebarMenus>,
name: String,
x: f64,
y: f64,
) -> Result<(), String> {
let menu = match name.as_str() {
"file" => &menus.file,
"edit" => &menus.edit,
_ => return Err(format!("unknown menu: {name}")),
};
window
.popup_menu_at(menu, LogicalPosition::new(x, y))
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let file = SubmenuBuilder::new(app, "ファイル")
.text("file:new", "新規作成")
.text("file:open", "開く...")
.separator()
.text("file:quit", "終了")
.build()?;
let edit = SubmenuBuilder::new(app, "編集")
.cut_with_text("切り取り")
.copy_with_text("コピー")
.paste_with_text("貼り付け")
.separator()
.select_all_with_text("すべて選択")
.build()?;
// macOS では同じメニューを画面上部のメニューバーに置く(先頭はアプリ名のメニュー)
#[cfg(target_os = "macos")]
{
let app_menu = SubmenuBuilder::new(app, "My Editor").about(None).separator().quit().build()?;
let bar = tauri::menu::MenuBuilder::new(app).items(&[&app_menu, &file, &edit]).build()?;
app.set_menu(bar)?;
}
app.manage(TitlebarMenus { file, edit });
Ok(())
})
.on_menu_event(|app, event| match event.id().as_ref() {
"file:quit" => app.exit(0),
id => println!("menu: {id}"), // 画面側で処理するなら emit で知らせる(menu-005)
})
.invoke_handler(tauri::generate_handler![open_titlebar_menu])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
for (const button of document.querySelectorAll<HTMLButtonElement>('[data-menu]')) {
button.addEventListener('mousedown', (e) => e.preventDefault());
button.addEventListener('click', () => {
const r = button.getBoundingClientRect();
void invoke('open_titlebar_menu', { name: button.dataset.menu, x: r.left, y: r.bottom });
});
}
Windows と Linux では、アプリのメニューを設定すると、メニューを個別に付けていない各ウィンドウにも割り当てられます。自作バーのメニューと役割が重なるので、例では set_menu() を macOS のときだけ呼んでいます。
動作確認
npm run tauri dev で起動すると、自作バーに「ファイル(F)」「編集(E)」が並びます。押すと、ボタンの左下の角に合わせてメニューが開き、拡大率を変えた画面でも位置はずれません。テキストエリアで文字を選び、「編集」→「コピー」、別の場所で「貼り付け」を選ぶと貼り付けられます。Alt+F で「ファイル」が開き、「新規作成」を選ぶか Ctrl+N を押すと、DevTools のコンソールに次のように出ます(Rust 版で項目を選んだときはターミナルに出ます)。
menu: file:new
macOS では自作のボタンが消え、同じ項目が画面上部のメニューバーに出ます。
よくあるエラーと対処法
- メニューがボタンではなくマウスの位置に開く:
popup()に位置を渡していません。 - 拡大率 150% などでメニューの位置がずれる: CSS の値を
PhysicalPositionで渡しています。LogicalPositionにします。 - 編集メニューのコピーや貼り付けが効かない: ボタンを押したときに入力欄からフォーカスが外れています。ボタンの
mousedownでpreventDefault()します。Linux ではlinux-libxdo機能も要ります(menu-007)。 - メニューに表示したショートカットを押しても反応しない: ポップアップで開くだけのメニューでは表示だけです。
keydownで処理します。 - 「menu.popup not allowed. Permissions associated with this command: core:menu:allow-popup, core:menu:default」: capability から
core:defaultを外しています(リリースビルドでは「Command plugin:menu|popup not allowed by ACL」)。core:menu:defaultを足します。
OS ごとの違いと注意点
- macOS: メニューは画面上部のメニューバーに 1 つ置くのが標準で、ウィンドウの中に並べる習慣はありません。例では自作のボタンを隠し、同じ項目を
setAsAppMenu()(Rust はset_menu())で置いています。信号機のボタンを残すならtitleBarStyle: "Overlay"にして左端を空けます(win-009)。先頭のサブメニューはアプリ名のメニューになります(Windows と macOS でメニューを出し分ける)。 - Windows / Linux: アプリのメニューを設定しないでおきます。自作バーを使うウィンドウとは別に標準のメニューバー付きのウィンドウも持つなら、そのウィンドウにだけ
setAsWindowMenu()で付けます。 - 見た目: 開くメニューの色や文字は OS のもので、CSS では変えられません。ページと同じ見た目にしたいなら HTML でドロップダウンを作りますが、その場合はウィンドウの外にはみ出して表示できません。
- ページ内の右クリックで同じ仕組みを使うなら 右クリックメニュー(コンテキストメニュー)を出す、見出しの作り方全般は アプリケーションのメニューバーを作る を参照してください。
