字玩FontPlayer开发笔记7 Tauri2动态切换菜单enbaled状态
字玩FontPlayer是笔者开源的一款字体设计工具,使用Vue3 + ElementUI开发,源代码:
github: https://github.com/HiToysMaker/fontplayer
gitee: https://gitee.com/toysmaker/fontplayer
笔记
字玩目前是用Electron进行桌面端应用打包,但是性能体验不太好,一直想替换成Tauri。Tauri的功能和Electron类似,都可以把前端代码打包生成桌面端(比如Windows和Mac)应用。Tauri只使用系统提供的WebView,不像Electron一样内置Chromium和Node.js,性能体验更佳。
前两天初步完成了Tauri配置和菜单设置,今天继续将原有Electron代码替换成Tauri。在字玩的设计中,菜单按钮的enbaled状态需要动态切换,比如导出图片只有在编辑模式下才能点击,在列表状态下是禁用的状态。原有基本逻辑是,在前端使用一个ref变量editStatus记录状态,使用watch监听editStatus改变,每当editStatus改变时,给Electron端发送消息,Electron端监听到消息后,根据设定好的规则更新菜单状态。虽然只是一个小功能,但是由于笔者对Rust和Tauri的生疏,还是费了不少功夫,在此记录一下。
定义禁用规则
在Rust端,对每个菜单按钮定义禁用规则,传入参数是前端发送过来的edit_status,根据不同的edit_status状态定义禁用规则。关于如何监听前端消息在下面会具体说明。
fn enable(edit_status: &str) -> bool {
true
}
fn enable_at_edit(edit_status: &str) -> bool {
match edit_status {
"edit" | "glyph" => true,
_ => false,
}
}
fn enable_at_list(edit_status: &str) -> bool {
match edit_status {
"edit" | "glyph" | "pic" => false,
_ => true,
}
}
fn template_enable(edit_status: &str) -> bool {
match edit_status {
"edit" | "glyph" | "pic" => false,
_ => true,
}
}
// 定义用于启用/禁用菜单项的映射
fn build_menu_enabled_map() -> HashMap<String, Box<dyn Fn(&str) -> bool>> {
let menu_enabled_map: HashMap<String, Box<dyn Fn(&str) -> bool>> = HashMap::from([
("about".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("create-file".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("open-file".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("save-file".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("save-as".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("undo".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("redo".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("cut".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("copy".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("paste".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("delete".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("import-font-file".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("import-glyphs".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("import-pic".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("import-svg".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("export-font-file".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("export-glyphs".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("export-jpeg".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("export-png".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("export-svg".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("add-character".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("add-icon".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("font-settings".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("preference-settings".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("language-settings".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("template-1".to_string(), Box::new(template_enable) as Box<dyn Fn(&str) -> bool>),
("remove-overlap".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
]);
menu_enabled_map
}
定义更新菜单状态方法
接下来定义更新菜单的方法,以供前端调用:
#[tauri::command]
fn toggle_menu_disabled(app: AppHandle, edit_status: String) {
let map = build_menu_enabled_map();
let window = app.get_webview_window("main").unwrap();
let menu = window.menu().unwrap();
for submenu in menu.items().unwrap() {
match submenu {
// 如果是 Submenu 类型,调用 items() 获取子菜单项
MenuItemKind::Submenu(submenu) => {
// 获取并遍历子菜单中的菜单项
for item in submenu.items().unwrap() {
match item {
MenuItemKind::MenuItem(item) => {
let id: String = item.id().0.clone();
let status: String = edit_status.clone();
let enabled: bool = map.get(&id).expect("Error")(&status);
item.set_enabled(enabled);
}
_ => {
// 如果是其他未处理的类型,使用 `_` 捕获
}
}
}
}
_ => {
// 如果是其他未处理的类型,使用 `_` 捕获
}
}
}
}
另外需要设置invoke_handler注册函数,这样前端才能调用:
.invoke_handler(tauri::generate_handler![toggle_menu_disabled])
前端调用Rust toggle_menu_disabled方法
在前端,监听editStatus变化,每当editStatus改变时,更新菜单enbaled状态。
const editStatusToString = (status: Status) => {
if (editStatus.value === Status.Edit) {
return 'edit'
} else if (editStatus.value === Status.Glyph) {
return 'glyph'
} else if (editStatus.value === Status.Pic) {
return 'pic'
}
return 'list'
}
watch(editStatus, () => {
invoke('toggle_menu_disabled', { editStatus: editStatusToString(editStatus.value) });
})
附完整Rust端代码
src-tauri/lib.rs
#![allow(unused)]
use tauri::{Manager, Window};
use tauri::Size;
use tauri::{AppHandle, Emitter};
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu, MenuItemBuilder, MenuItemKind};
use std::collections::HashMap;
#[tauri::command]
fn test(app: AppHandle) {
app.emit("create-file", ()).unwrap();
}
fn enable(edit_status: &str) -> bool {
true
}
fn enable_at_edit(edit_status: &str) -> bool {
match edit_status {
"edit" | "glyph" => true,
_ => false,
}
}
fn enable_at_list(edit_status: &str) -> bool {
match edit_status {
"edit" | "glyph" | "pic" => false,
_ => true,
}
}
fn template_enable(edit_status: &str) -> bool {
match edit_status {
"edit" | "glyph" | "pic" => false,
_ => true,
}
}
// 定义用于启用/禁用菜单项的映射
fn build_menu_enabled_map() -> HashMap<String, Box<dyn Fn(&str) -> bool>> {
let menu_enabled_map: HashMap<String, Box<dyn Fn(&str) -> bool>> = HashMap::from([
("about".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("create-file".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("open-file".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("save-file".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("save-as".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("undo".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("redo".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("cut".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("copy".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("paste".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("delete".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("import-font-file".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("import-glyphs".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("import-pic".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("import-svg".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("export-font-file".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("export-glyphs".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("export-jpeg".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("export-png".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("export-svg".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
("add-character".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("add-icon".to_string(), Box::new(enable_at_list) as Box<dyn Fn(&str) -> bool>),
("font-settings".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("preference-settings".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("language-settings".to_string(), Box::new(enable) as Box<dyn Fn(&str) -> bool>),
("template-1".to_string(), Box::new(template_enable) as Box<dyn Fn(&str) -> bool>),
("remove-overlap".to_string(), Box::new(enable_at_edit) as Box<dyn Fn(&str) -> bool>),
]);
menu_enabled_map
}
#[tauri::command]
fn toggle_menu_disabled(app: AppHandle, edit_status: String) {
let map = build_menu_enabled_map();
let window = app.get_webview_window("main").unwrap();
let menu = window.menu().unwrap();
for submenu in menu.items().unwrap() {
match submenu {
// 如果是 Submenu 类型,调用 items() 获取子菜单项
MenuItemKind::Submenu(submenu) => {
// 获取并遍历子菜单中的菜单项
for item in submenu.items().unwrap() {
match item {
MenuItemKind::MenuItem(item) => {
let id: String = item.id().0.clone();
let status: String = edit_status.clone();
let enabled: bool = map.get(&id).expect("Error")(&status);
item.set_enabled(enabled);
}
_ => {
// 如果是其他未处理的类型,使用 `_` 捕获
}
}
}
}
_ => {
// 如果是其他未处理的类型,使用 `_` 捕获
}
}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 获取名为 "main" 的 Webview 窗口句柄
let window = app.get_webview_window("main").unwrap();
// 获取窗口尺寸
let primary_display = window.inner_size().unwrap();
let screen_width = primary_display.width;
let screen_height = primary_display.height;
// 获取主显示器的 DPI 缩放因子
let scale_factor = window.scale_factor().unwrap();
// 设置最大窗口尺寸
let max_width = 1280;
let max_height = 800;
// 根据 DPI 缩放因子调整窗口尺寸
let adjusted_width = (max_width as f64 * scale_factor) as u32;
let adjusted_height = (max_height as f64 * scale_factor) as u32;
// 计算窗口大小,确保窗口大小不超过屏幕大小
let window_width = screen_width.min(adjusted_width);
let window_height = screen_height.min(adjusted_height);
// 获取窗口并设置尺寸
window.set_size(Size::new(tauri::PhysicalSize::new(window_width as u32, window_height as u32))).unwrap();
app.on_menu_event(move |app, event| {
if event.id() == "about" {
println!("about");
} else if event.id() == "create-file" {
test(app.app_handle().clone())
}
});
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.menu(|handle| Menu::with_items(handle, &[
&Submenu::with_items(
handle,
"File",
true,
&[
&MenuItemBuilder::with_id("about", "关于").build(handle).expect("Error")
],
)?,
&Submenu::with_items(
handle,
"文件",
true,
&[
&MenuItemBuilder::with_id("create-file", "新建工程").build(handle).expect("Error"),
&MenuItemBuilder::with_id("open-file", "打开工程").build(handle).expect("Error"),
&MenuItemBuilder::with_id("save-file", "保存工程").build(handle).expect("Error"),
&MenuItemBuilder::with_id("save-as", "另存为").build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"编辑",
true,
&[
&MenuItemBuilder::with_id("undo", "撤销").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("redo", "重做").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("cut", "剪切").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("paste", "粘贴").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("copy", "复制").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("delete", "删除").enabled(false).build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"导入",
true,
&[
&MenuItemBuilder::with_id("import-font-file", "导入字体库").build(handle).expect("Error"),
&MenuItemBuilder::with_id("import-glyphs", "导入字形").build(handle).expect("Error"),
&MenuItemBuilder::with_id("import-pic", "识别图片").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("import-svg", "导入SVG").enabled(false).build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"导出",
true,
&[
&MenuItemBuilder::with_id("export-font-file", "导出字体库").build(handle).expect("Error"),
&MenuItemBuilder::with_id("export-glyphs", "导出字形").build(handle).expect("Error"),
&MenuItemBuilder::with_id("export-jpeg", "导出JPEG").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("export-png", "导出PNG").enabled(false).build(handle).expect("Error"),
&MenuItemBuilder::with_id("export-svg", "导出SVG").enabled(false).build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"字符与图标",
true,
&[
&MenuItemBuilder::with_id("add-character", "添加字符").build(handle).expect("Error"),
&MenuItemBuilder::with_id("add-icon", "添加图标").build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"设置",
true,
&[
&MenuItemBuilder::with_id("font-settings", "字体设置").build(handle).expect("Error"),
&MenuItemBuilder::with_id("preference-settings", "偏好设置").build(handle).expect("Error"),
&MenuItemBuilder::with_id("language-settings", "语言设置").build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"模板",
true,
&[
&MenuItemBuilder::with_id("template-1", "测试模板").build(handle).expect("Error"),
],
)?,
&Submenu::with_items(
handle,
"工具",
true,
&[
&MenuItemBuilder::with_id("remove-overlap", "去除重叠").enabled(false).build(handle).expect("Error"),
],
)?,
]))
.invoke_handler(tauri::generate_handler![toggle_menu_disabled])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}