文档

macOS

NSH 应用也能作为原生 Mac 应用运行。macOS 后端用 AppKit 视图和 Core Animation 渲染应用,用 CoreText 排版文字;核心、响应式图、 渲染器、Taffy 布局和 nsh/ui/mobile 组件与 iOS、Android 上完全相同,因此同一个 bundle 可以在所有平台上运行。在 macOS 上, Platform.OS 为 "macos",并会应用 desktop={{…}} 属性。窗口标题、自定义标题栏、 菜单栏和右键菜单由 nsh/desktop 提供。

macOS 后端的行为是桌面应用:鼠标、触控板和键盘输入,可调整大小的窗口,居中的表单和菜单栏。

#环境要求

  • 运行应用的机器需要 macOS 14 及以上。
  • Xcode 命令行工具(xcode-select --install),用于从源码构建 NSH 的 macOS 运行时以及打包应用(codesign、sips)。 在 Mac 上用 cargo xtask dist 制作的发行版把运行时放在 runtime/macos/ 中。

#运行应用

nsh run my-app --target macos

应用会在一个 AppKit 窗口(默认 1200×800)中打开,和其他目标一样支持热重载、在终端显示日志并提供错误浮层;Cmd+R 重新加载最新构建。 菜单栏和窗口显示应用的名称(nsh.json 的 "name",否则是应用目录的名称)。在源码仓库中,nsh run 会先构建开发宿主(nsh-macos-host)。停止 nsh run 会关闭窗口,关闭窗口也会结束 nsh run。 宿主也可以单独运行一个构建好的 bundle:

nsh build my-app --release -o build
dist/nsh/runtime/macos/nsh-macos-host --bundle build/bundle.nshc --size 1200x800   # 源码仓库中:target/debug/nsh-macos-host

--size 是窗口的初始尺寸;窗口可以调整大小。

#性能监视器

--fps(nsh run my-app --target macos --fps、宿主的 --fps、打包后应用的 open -a MyApp --args --fps,嵌入时用 AppOptions { fps: true, .. } / AppView::set_fps_monitor(true))会在应用右上角显示一个每秒刷新两次的面板:

行测量内容
FPS最近一秒显示的帧数:应用有变化的屏幕刷新次数(空闲的应用读数低,这不代表慢)。max 是主线程能跟上的帧率,之后是显示器的刷新率(ProMotion 屏幕为 120 Hz)。
TICK屏幕刷新实际到达的间隔(均值、p95),以及 gap:显示帧之间间隔的 p95。没有迟到时为一次刷新的时长(120 Hz 下 8.3 ms)。
MAIN每帧的主线程耗时(应用引擎的变更、绘制文本和画布、动画、输入)、它的 95 分位数,以及 drop:超过一次刷新时长的帧所占比例。
JS / CORE每次 commit:你的代码(事件处理、定时器、列表行)和 NSH 核心(布局、文本、diff)的耗时;每秒 commit 数、每次 commit 的原生视图变更数。
HOP变更在引擎线程和主线程之间等待的时间;引擎线程的繁忙程度。
CPU / GPU / MEM进程:CPU(100% 为一个核心)、它占用的 GPU 比例、内存占用(活动监视器中的数值)。

下方的柱状图是最近 120 帧的主线程耗时,与一次刷新的时长(横线)对比:在一半以内为绿色,在其以内为琥珀色,超过为红色。 同时加上 --trace 时,宿主还会每秒把读数记录到日志一次(毫秒,依次为 mean/p50/p95/max)。请测量 release 构建;监视器关闭时没有任何开销。

#打包

nsh package my-app --target macos [--run]

会写出一个应用包:

out/<Name>.app/Contents/
  Info.plist
  MacOS/<Name>               可执行文件(nsh 的发布版运行器)
  Resources/nsh/bundle.nshc  编译后的应用
  Resources/nsh/assets/…     应用导入的资源
  Resources/nsh/app.json     窗口标题和应用的外观
  Resources/AppIcon.icns     图标(nsh.json 指定了图标时,用 sips 转换)

.app 使用 ad hoc 签名,因此可以在构建它的 Mac 上运行。要分发它,请用 Developer ID 签名并进行公证。--run 会在打包后打开应用。

#嵌入 Mac 应用

nsh-platform-macos crate(位于 NSH 仓库中)为 Rust 编写的 Mac 应用提供 AppView:一个作为 NSView 的 NSH 引擎。每个 AppView 相互独立,可以同时运行多个。

use std::rc::Rc;
use nsh_platform_macos::{
    objc2::MainThreadMarker, objc2_app_kit::NSWindow, AppOptions, AppView, AssetBase, Bundle,
};

fn show(window: &NSWindow) -> Rc<AppView> {
    let mtm = MainThreadMarker::new().expect("main thread");
    let view = AppView::new(mtm, AppOptions {
        asset_base: Some(AssetBase::Directory("app/assets".into())),
        ..AppOptions::default()
    });
    // 在加载之前注册原生组件
    let code = std::fs::read_to_string("app/bundle.js").expect("bundle");
    view.load(Bundle::source(code).into());
    window.setContentView(Some(&view.view()));
    // 窗口显示视图期间保留它;窗口关闭时调用 `stop`。
    view
}

AppView::load 会替换正在运行的内容;stop 结束应用。asset_base 指定 asset:/ URI 从哪里解析(AssetBase::Directory 或 AssetBase::Http)。字节码 bundle(bundle.nshc)用 unsafe { Bundle::bytecode(bytes) } 加载:字节码是受信任的输入, 只能从你自己应用的文件中加载。如果应用只是一个 NSH 应用,run_app(source, options) 会在窗口中打开一个 AppView,带有标准菜单 (关于、隐藏、退出、编辑、最小化、缩放、进入全屏、关闭),应用可以用 Menu.setApplicationMenu (nsh/desktop)替换它,并运行到窗口关闭为止。

原生组件通过 AppView::register_component 注册,见原生组件。

#macOS 上的差异

运行环境是桌面环境:安全区和键盘高度为 0,Platform.constants.interfaceIdiom 为 "desktop",Platform.Version 为 macOS 的版本号, 深色模式实时跟随系统外观。见设备与环境。

输入是桌面输入:鼠标、触控板(带惯性的滚动、捏合和旋转)和键盘。selectable 文本通过拖动、双击和三击选择,用 Cmd+C 复制。 Escape、Option+Left、Cmd+[ 和鼠标的后退键会在栈中返回,或请求模态视图关闭(焦点在输入框或网页中时,Escape 只会离开它; 网页先收到这些按键,它自己用掉的不会触发返回);pageSheet 和 formSheet 是窗口中居中的卡片,背后的内容变暗。 点击 Input 的任意位置(包括内边距)都会把光标放到最近的位置。 Alert 和 ActionSheet 以 NSAlert 表单的形式显示在窗口上:不超过两个按钮的提示框按钮排成一行(取消按钮在最左边,最后一个普通按钮是 Return 的默认按钮);更多按钮以及操作表的选项按顺序竖排,取消按钮在最后,没有默认按钮;Escape 按下取消按钮。

#键盘

按键作用
Return在 Input 中按 submitBehavior 的设定:提交(onSubmitEditing)、提交并离开输入框,或换行(多行输入框)
Shift+Return、Option+Return、Ctrl+Return在 Return 用于提交的多行输入框中换行
Tab、Shift+Tab(加 Ctrl 亦可)按屏幕顺序切换到下一个 / 上一个输入框,限于最新的模态视图内;Option+Tab 在多行输入框中输入制表符
Escape离开有焦点的输入框;否则清除文本选区,再返回或请求最新的模态视图关闭
Option+Left、Cmd+[、鼠标后退键返回(输入框有焦点时不会:此时 Option+Left 移动光标)
Option / Cmd + 方向键(可加 Shift);Option+Delete、Cmd+Delete;Ctrl+A / E / K在输入框中按词和按行移动、选择和删除,与所有 Mac 应用一致
Cmd+Z、Shift+Cmd+Z、Cmd+X / C / V / A撤销、重做、剪切、复制、粘贴、全选(密码框拒绝剪切和复制);Cmd+A / Cmd+C 选择并复制 selectable 文本
方向键;Page Up / Down、Space、Shift+Space、Option+Up / Down;Home / End、Cmd+Up / Down滚动有焦点的视图所在或鼠标下方的滚动视图:一行、一页、到开头 / 结尾(输入框有焦点时不滚动)
Cmd+W、Cmd+M、Ctrl+Cmd+F、Cmd+H、Option+Cmd+H、Cmd+Q关闭、最小化、全屏、隐藏、隐藏其他、退出(nsh package 和 nsh run 应用的菜单)
Cmd+R重新加载最新的构建(nsh run)

应用自己的快捷键(nsh/keymap)先于以上所有按键处理,但退出、隐藏、最小化和全屏不能被绑定。

快捷键在任何键盘布局下都可用(在西里尔或希腊字母布局下,Cmd+C 是美式布局中输入 C 的那个键)。确认输入法组字(中文、日文等)的 Return 不会提交。onKeyPress 报告的内容与 React Native 一致:输入的文本、"Enter"、"Backspace" 以及 "Escape"。

不支持或有所不同:

  • 可缩放的滚动视图(zoomTo、缩放相关属性)。
  • mixBlendMode,以及大于 1 的 brightness()。
  • 输入框:内容类型与自动填充、键盘类型、自动大写、字间距;输入辅助视图从不显示(没有软键盘),keyboardShouldPersistTaps 被忽略。
  • WebView 的 scrollEnabled={false} 让鼠标滚轮和触控板不再滚动页面,而是像 iOS 上一样滚动它外面的应用滚动视图。 页面仍可用键盘(空格、方向键)和它自己的脚本滚动,触控板在页面之间的轻扫(allowsBackForwardNavigationGestures)也会停用。
  • 无障碍:自定义 accessibilityActions。
  • 状态栏和触感反馈(桌面上没有对应物)。

#另见