Web 技术非常擅长写 GUI,所以诞生了 Atom,Electron 等框架,几年前出现了一个新的框架 Tauri,使用 Rust + 系统 WebView,在前段时间发布了 1.0 版本,于是我将 NESBox PWA 复制了一个 Tauri App,下载地址。
由于 App 只有一个窗口,所以和 PWA 几乎一样,就连本来不一样的标题栏也改成了一样。但是,踩的坑不少。
定制标题栏
默认 Tauri App 是一个很小的标题栏,所以需要先隐藏标题栏,自己在 Web 中画一个,刚画完,就发现一个拖动光标的问题(现在已经修复),由于我是用的自定义元素,所以不能使用 attribute 让 Tauri 自动完成拖动功能,得手动处理 :
this.addEventListener('mousedown', (event) => {
this.#window?.startDragging();
// https://github.com/tauri-apps/tauri/issues/4059#issuecomment-1154360504
event.preventDefault();
});
但是这个方案会阻止 Window 下双击标题栏效果,所以得监听 `mousedown` 处理双击事件。
标题栏画好后,macOS 下的交通灯默认是在小标题栏下的位置,距离 App 边框间距很小,修改间距是一个棘手的问题, 原因 Tauri 现在没有 Electron 那样丰富的 API,好在 Rust 有 Objective-C 的运行时包装库,可以利用他调用 Appkit 框架的 API,例如前面提到的交通灯间距问题,只需要创建一个新的 Toolbar,交通灯就自动对齐新的 Toolbar 而改变了位置:
id.setToolbar_(msg_send![class!(NSToolbar), new]);
然而,Toolbar 会在全屏时显示,尽管它没有任何内容,所以需要在全屏时隐藏 Toolbar,Tauri 提供了 window resize 事件,可以在回调中判断是否全屏来执行相应动作,Tauri 判断全屏 API 有 bug,所以需要手动判断。
tauri::WindowEvent::Resized(size) => {
let monitor = event.window().current_monitor().unwrap().unwrap();
let screen = monitor.size();
println!("{:?}, {:?}", size, screen);
if size == screen {
println!("{}", true);
} else {
println!("{}", false);
}
}
最后,Web 的 UI 需要适应 Web 标题栏,比如 Modal 蒙层尺寸,Toast 的位置,这里使用一个类似 env 的 CSS 变量 --titlebar-area-height
解决,在 UI 库中将它作为一个通用解决方案:
top: calc(1em + env(titlebar-area-height, var(--titlebar-area-height, 0px)));
解决了上述问题后,标题栏才算定制完成,可以查看完整代码。
App 启动优化
Tauri App 启动时,会闪白屏,如果应用是暗黑主题的将不可忍受,有提供 Splashscreeen 方案,但是这个方案非常不优雅,他本质上是用另一个窗口显示启动动画,当我们的 WebApp 写得足够好时,首屏加载是非常快的,所以根本不需要类似 Photoshop、Cinema 4D 这样体验的启动动画。
Tauri 有 on_page_load
回调,先让窗口不可显示,然后在回调中显示窗口,但它也有问题:
- 在 macOS 下,它并不是很可靠,偶尔还是能看到闪白屏
- 在 Windows 下,将和 set_decorations 冲突导致 App 无响应
于是在 Windows 下,利用平台 Tauri 配置,不使用此方案。
就像评论中说的一样,更好的方案是配置 WebView 初始背景颜色,但是目前 wry(Tauri 的 Webview 抽象库) 还不支持,在 macOS 下,可以使用 objc 动态配置 WebView 颜色,配合 on_page_load
效果还可以:
let id = self.ns_window().unwrap() as cocoa::base::id;
let color = NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0.0, 0.0, 0.0, 1.0);
let _: cocoa::base::id = msg_send![id, setBackgroundColor: color];
self.with_webview(|webview| {
// !!! has delay
let id = webview.inner();
let no: cocoa::base::id = msg_send![class!(NSNumber), numberWithBool:0];
let _: cocoa::base::id =
msg_send![id, setValue:no forKey: NSString::alloc(nil).init_str("drawsBackground")];
})
.ok();
Window 下没有好的解决方案。
播放音效
作为一个游戏平台,音效是一个很重要的功能,Web 中播放的声音还是得遵守 Web 的自动播放策略,但也可以利用 Tauri 的 API 来使用 rodio 播放声音,这可以绕过 Web 自动播放策略:
#[command]
pub async fn play_sound(kind: &str, volume: f32) -> Result<(), String> {
if volume < 0.03 {
return Ok(());
}
let (_stream, stream_handle) = OutputStream::try_default().map_err(|_| "".to_string())?;
let sink = Sink::try_new(&stream_handle).map_err(|_| "".to_string())?;
let file = match kind {
"new_invite" => Cursor::new(include_bytes!("new_invite.aac").as_ref()),
_ => Cursor::new(include_bytes!("new_message.aac").as_ref()),
};
let source = Decoder::new(file).map_err(|_| "".to_string())?;
sink.set_volume(volume);
sink.append(source);
sink.sleep_until_end();
Ok(())
}
注意这里使用异步函数播放,否则会阻塞主线程。
设置徽章
Tauri 现在还没有类似 Web / Electron 的徽章(例如 Dock、任务栏显示未读消息)的 API,在 macOS 中,然后可以通过 objc 创建一个简单的 setBadge API:
#[command]
pub fn set_badge(count: i32) {
#[cfg(target_os = "macos")]
unsafe {
let label = if count == 0 {
nil
} else {
NSString::alloc(nil).init_str(&format!("{}", count))
};
let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile];
let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label];
}
}