API

nsh/desktop

nsh/desktop 包含只有桌面才有的东西:窗口和它的标题栏、应用菜单和右键菜单。它在 macOS、Windows 和 Linux 上可用。这个模块在所有平台上都可以导入,因此同一个 bundle 可以在所有平台运行:在 iOS 和 Android 上它的函数什么也不做,isAvailable 告诉你当前是否在桌面上。

import { isAvailable, Menu, titleBar, Window } from "nsh/desktop";

#isAvailable

const isAvailable: boolean;

在 macOS、Windows 和 Linux 上为 true,其他平台为 false。

#Window

const Window: {
  setTitle(title: string): void;
  setTitleBar(style: "default" | "hidden", height?: number): void;
  minimize(): void;
  toggleMaximize(): void;
  toggleFullScreen(): void;
  close(): void;
};
成员说明
setTitle(title)窗口的标题:标题栏、任务栏或 Dock、窗口切换器里显示的名字。
setTitleBar(style, height?)"hidden" 去掉系统标题栏:应用一直铺到窗口顶边,自己绘制标题栏。系统的窗口按钮保留(macOS 的红绿灯、Windows 的最小化 / 最大化 / 关闭按钮、GTK 的窗口按钮)。height 是你的标题栏高度(单位为点):窗口按钮在其中垂直居中(Windows 的标题栏按钮撑满这个高度);省略时使用平台的高度。"default" 恢复系统标题栏。
minimize()最小化窗口。
toggleMaximize()最大化窗口,已最大化时还原。
toggleFullScreen()进入或退出全屏。
close()关闭窗口,应用随之结束。

#自定义标题栏

隐藏标题栏后,应用绘制自己的标题栏,并用 desktop={{ windowDrag: true }} 指定哪些部分可以拖动窗口:

import { Input, Text, View } from "nsh";
import { titleBar, Window } from "nsh/desktop";

Window.setTitleBar("hidden", 40); // 40 点高的标题栏

function TitleBar() {
  return (
    <View
      desktop={{ windowDrag: true }}
      style={{
        height: titleBar().height || 40,
        paddingLeft: titleBar().left + 8, // macOS 的红绿灯
        paddingRight: titleBar().right + 8, // Windows 的标题栏按钮、GTK 的窗口按钮
        flexDirection: "row",
        alignItems: "center",
      }}
    >
      <Text>My App</Text>
      <View style={{ flex: 1 }} />
      <Input placeholder="搜索" style={{ width: 180 }} />
    </View>
  );
}

按住带 windowDrag 的视图会像按住系统标题栏一样拖动窗口:双击最大化(或执行用户的设置:macOS 的“连按窗口标题栏以”),在 Windows 和 Linux 上右键打开窗口菜单。

  • 由最近的设置了 windowDrag 的视图决定:desktop={{ windowDrag: false }} 让可拖动视图中的一部分保持可点击。
  • 可拖动视图中的文本框,以及带按压或触摸处理(onPress、onTouch*、responder)的视图,不用设置也保持可点击:上例中的搜索框和其中的按钮照常工作。
  • windowDrag 在系统标题栏下也能用:侧边栏或工具栏同样可以拖动窗口。

#titleBar

const titleBar: Accessor<{ height: number; left: number; right: number }>;

隐藏的标题栏的区域,单位为点(响应式):它的 height,以及系统窗口按钮在左端(left)和右端(right)占用的宽度。用它来布局标题栏,避免内容被窗口按钮遮住。系统显示标题栏时(以及全屏时、非桌面平台上)三者都为 0。

heightleftright
macOSheight,否则为标题栏的高度(28 或 32,视 macOS 版本而定)红绿灯的宽度0
Windowsheight,否则为 320三个标题栏按钮(138)
Linuxheight,否则为标题栏(header bar)的高度开头的窗口按钮(按桌面的按钮布局)末尾的窗口按钮

在 Windows 上,标题栏按钮由 nsh 按 Windows 11 的样式绘制,跟随应用的外观;鼠标停在最大化按钮上会出现 Windows 11 的贴靠布局。在所有平台上窗口都保留调整大小的边框、阴影和圆角。

const Menu: {
  setApplicationMenu(items: MenuItem[] | null): void;
  popup(items: MenuItem[], at?: { x: number; y: number }): Promise<void>;
};
成员说明
setApplicationMenu(items)设置应用菜单:每个顶层项是一个子菜单(File、Edit……)。在 macOS 上是菜单栏,第一个菜单是应用菜单(AppKit 以应用名作为标题)。在 Windows 和 Linux 上是窗口的菜单栏,隐藏标题栏时不显示;它的快捷键仍然有效,自定义标题栏可以用 Menu.popup 打开这些菜单。null 恢复平台默认菜单。
popup(items, at?)显示右键菜单,左上角在 at(窗口坐标,单位为点),省略时在指针处。菜单关闭时(无论是否选中了菜单项)返回的 Promise 会 resolve:打开它的按钮可以在此之前一直保持按下样式。

再次调用 setApplicationMenu 即可修改菜单。放在 effect 里时,它会跟随读取的 signal:依赖应用状态的勾选或可用状态始终保持最新。

#菜单项

type MenuItem =
  | { type: "separator" }
  | {
      label?: string;
      role?: MenuRole;
      shortcut?: string;
      enabled?: boolean;
      checked?: boolean;
      onSelect?: () => void;
      submenu?: MenuItem[];
    };
字段说明
label菜单项显示的文字。带 role 的菜单项省略时使用自己的默认文字。
onSelect菜单项被选中时调用,无论是点击还是快捷键。
shortcut快捷键:修饰键和一个按键用 + 连接,例如 "mod+n"、"mod+shift+z"、"alt+f4" 或 "f11"。修饰键见下表。按键可以是字母、数字和标点,写作它在美式键盘上不按 Shift 时输入的字符(,、/;? 写作 "shift+/"),也可以是名称:plus(= / + 键,也可写 =)、minus(也可写 -)、space、enter(return)、escape(esc)、tab、backspace、delete(del)、insert、方向键 up、down、left、right(arrowup 等)、home、end、pageup、pagedown、f1–f24。大小写不影响("Mod+Shift+N" 就是 "mod+shift+n";大写字母表示同一个键,并不意味着 Shift),推荐全小写的写法。无效的快捷键("mod-n"、"mod++"、"mod+?")会抛出错误并给出正确的写法。nsh/keymap 使用同样的按键写法。菜单按各平台自己的样式显示快捷键(macOS 上是 ⇧⌘N,Windows 上是 Ctrl+Shift+N)。
enabledfalse 时菜单项变灰(默认 true)。
checked设置(为 true 或 false)后菜单项带勾选标记。
submenu让菜单项成为子菜单(需要 label)。
role由平台执行的标准菜单项(见下)。

null、undefined 和 false 项会被忽略,因此可以在列表中写 cond && { … }。

修饰键:

修饰键macOSWindowsLinux
mod(也可写 cmdorctrl)Command(⌘)CtrlCtrl
cmd(也可写 command、meta、super、win)Command(⌘)Windows 键Super
ctrl(也可写 control)Control(⌃)CtrlCtrl
alt(也可写 option、opt)Option(⌥)AltAlt
shiftShift(⇧)ShiftShift

应用自己的快捷键请用 mod:它就是各平台应用习惯使用的那个键(macOS 上是 ⌘N,Windows 和 Linux 上是 Ctrl+N),一种写法到处适用。Windows 键和 Super 基本归系统使用(Win+E、GNOME 的活动视图),所以在 macOS 以外很少该用 cmd。

Role:

Role作用平台
about、hide、hideOthers、showAll应用菜单的标准项macOS(其他平台忽略)
quit退出应用全部
undo、redo、cut、copy、paste、selectAll编辑获得焦点的文本框(快捷键仍归文本框自己)全部
minimize、zoom、toggleFullScreen、close窗口操作全部

带 role 的菜单项默认使用平台的文字和快捷键,除非自己设置(macOS 会在 about、hide 和 quit 中加上应用名)。

菜单快捷键的优先级最高,和原生应用一样:菜单项占用的按键不会再传给获得焦点的输入框,也不会触发 nsh 自己的按键处理。只有 nsh/keymap 中相同按键组合的绑定排在它们前面。所以编辑快捷键(mod+c、mod+v、mod+x、mod+a、mod+z)只交给编辑类 role(它们会把按键转给输入框),也不要使用不带修饰键的键(escape、方向键、字母),否则输入框和返回导航就收不到它们了。

import { effect, signal } from "nsh";
import { Platform } from "nsh/device";
import { Menu } from "nsh/desktop";

const mac = Platform.OS === "macos";
const wrap = signal(true);

effect(() => {
  Menu.setApplicationMenu([
    mac && {
      label: "My App",
      submenu: [{ role: "about" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { type: "separator" }, { role: "quit" }],
    },
    {
      label: "File",
      submenu: [
        { label: "New", shortcut: "mod+n", onSelect: () => createDocument() },
        { label: "Open…", shortcut: "mod+o", onSelect: () => openDocument() },
        !mac && { type: "separator" },
        !mac && { role: "quit", label: "Exit" },
      ],
    },
    {
      label: "Edit",
      submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }],
    },
    {
      label: "View",
      submenu: [
        { label: "Word Wrap", checked: wrap(), onSelect: () => wrap.update((w) => !w) },
        { role: "toggleFullScreen" },
      ],
    },
  ]);
});

在按钮按下时弹出右键菜单:

<View
  onPress={() =>
    Menu.popup([
      { label: "重命名", onSelect: rename },
      { label: "复制一份", onSelect: duplicate },
      { type: "separator" },
      { label: "删除", onSelect: remove },
    ])
  }
>
  <Text>更多</Text>
</View>