修复 release 默认网页右键菜单

release 客户端关闭 WebView 默认上下文菜单

保留 contextmenu 事件供业务组件处理

补充 release/debug 行为测试与技术方案约定
This commit is contained in:
2026-08-26 21:19:39 +08:00
parent a8d4aae390
commit 16a5d28ed3
3 changed files with 87 additions and 1 deletions
@@ -0,0 +1,84 @@
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
/// Release 客户端只取消浏览器的默认上下文菜单。
///
/// 这里刻意只调用 `preventDefault``contextmenu` 事件仍会继续沿 DOM
/// 传播,业务组件可以照常接收并打开自己的菜单。捕获阶段用于覆盖没有
/// 自己右键处理的页面空白区域,但不会调用 `stopPropagation`。
const RELEASE_CONTEXT_MENU_INIT_SCRIPT: &str = r#"
document.addEventListener(
'contextmenu',
(event) => {
event.preventDefault();
},
true,
);
"#;
fn context_menu_init_script(is_release: bool) -> Option<&'static str> {
is_release.then_some(RELEASE_CONTEXT_MENU_INIT_SCRIPT)
}
#[cfg(all(windows, not(debug_assertions)))]
fn disable_windows_default_context_menus<R: Runtime>(webview: tauri::Webview<R>) {
let _ = webview.with_webview(|platform_webview| {
// WebView2 的原生设置只关闭浏览器菜单,不会取消 DOM 的
// `contextmenu` 事件,因此业务右键菜单仍然可以正常工作。
// SAFETY: Tauri 在 WebView2 UI 线程上提供当前存活的 controller
// 这些 COM 接口调用只使用该闭包期间有效的句柄。
let _ = unsafe {
platform_webview
.controller()
.CoreWebView2()
.and_then(|webview| webview.Settings())
.and_then(|settings| settings.SetAreDefaultContextMenusEnabled(false))
};
});
}
/// 注册 release-only 的默认网页上下文菜单策略。
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
let mut builder = Builder::new("release-context-menu");
if let Some(script) = context_menu_init_script(!cfg!(debug_assertions)) {
builder = builder.js_init_script_on_all_frames(script);
}
#[cfg(all(windows, not(debug_assertions)))]
{
builder = builder.on_webview_ready(disable_windows_default_context_menus);
}
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn release_build_gets_a_context_menu_init_script() {
assert_eq!(
context_menu_init_script(true),
Some(RELEASE_CONTEXT_MENU_INIT_SCRIPT)
);
}
#[test]
fn debug_build_keeps_the_default_context_menu_strategy_untouched() {
assert_eq!(context_menu_init_script(false), None);
}
#[test]
fn init_script_prevents_only_the_default_action() {
let script = RELEASE_CONTEXT_MENU_INIT_SCRIPT;
assert!(script.contains("contextmenu"));
assert!(script.contains("event.preventDefault()"));
assert!(script.contains("true"));
assert!(!script.contains("stopPropagation"));
assert!(!script.contains("stopImmediatePropagation"));
}
}
@@ -58,6 +58,7 @@ mod command_sandbox_trampoline;
mod commands;
mod config;
mod context_compaction;
mod context_menu;
#[cfg(all(debug_assertions, not(test)))]
mod debug;
mod delegation;
@@ -2021,6 +2022,7 @@ fn main() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(context_menu::init())
.manage(game_creator_preview_registry())
.manage(ProjectResourcePreviewReadManager::default())
.setup(move |app| {
@@ -50,7 +50,7 @@ Runtime 确认卡与普通聊天确认卡必须共用“信息区 + 固定操作
此 seam 的纯测试覆盖嵌套 context、offset 反算、同树重挂载、子节点页面位置保持与 Inspector anchor preset 保位;几何修复应在此 Module 局部验证后由三个 adapter 复用。
左侧 `UI Tree` 与预览画布复用同一个节点右键菜单组件和条目模型,顺序固定为“新增子节点 → 新增同级节点 → 删除节点及子节点”。页面根仅显示“新增子节点”;虚拟超级根不提供菜单。预览仅在“编辑叠加”模式中响应已渲染节点的鼠标右键,右键会选中该节点并阻止嵌套节点事件冒泡;“最终预览”以及画布空白处保留原有行为,空白处不接管浏览器原生菜单
左侧 `UI Tree` 与预览画布复用同一个节点右键菜单组件和条目模型,顺序固定为“新增子节点 → 新增同级节点 → 删除节点及子节点”。页面根仅显示“新增子节点”;虚拟超级根不提供菜单。预览仅在“编辑叠加”模式中响应已渲染节点的鼠标右键,右键会选中该节点并阻止嵌套节点事件冒泡;“最终预览”以及画布空白处不新增业务菜单。debug/dev 构建保留浏览器默认网页上下文菜单,release 客户端关闭 WebView 的默认网页菜单,但仍派发 `contextmenu` 事件给业务组件,不以停止事件传播的方式屏蔽右键交互
菜单由页面级 portal 呈现,使用屏幕坐标且在视口边缘内收,避免受预览画布缩放和容器裁切影响。点击菜单外、按 Escape、窗口失焦、滚动或调整窗口大小均关闭菜单。全局 `controller.editor.isLocked` 时,节点仍可选中和查看菜单,但所有结构修改项禁用;状态层的 mutation 校验继续作为最终防线。点击可用项先关闭菜单再执行现有 controller 动作,不改变既有新增或删除后的选择策略。