本頁由機器翻譯,可能有錯誤。 閱讀英文原文

API

nsh/desktop

nsh/desktop 收錄只有桌面平台才有的功能:視窗與其標題列、應用程式選單和右鍵選單,可在 macOS、Windows 和 Linux 上運作。Rust 程式碼可在 nsh_core::element::desktop 找到相同的內容:視窗的函式(set_title、set_title_bar、title_bar、minimize、… close_window)、應用程式選單與右鍵選單(set_application_menu、popup_menu,由多個 MenuItem 組成),並以 .window_drag(true) 標記可拖曳的 view;TSX 則匯入 nsh/desktop。此模組可在所有平台上匯入,因此同一個 bundle 可以到處執行:在 iOS 和 Android 上,它的函式不會執行任何動作,而 isAvailable 會告訴你視窗與選單是否可用。

use nsh_core::{
    desktop::{MenuRole, TitleBarStyle},
    element::desktop::{
        MenuItem, close_window, popup_menu, set_application_menu, set_title, set_title_bar, title_bar,
    },
    prelude::*,
};

#isAvailable

const isAvailable: boolean;

true 表示此模組的視窗與選單 API 可運作的環境:macOS、Windows 和 Linux;其他環境則為 false。Rust:os().is_desktop()。

platform.desktop 是針對桌面行為(desktop={{…}} props、platform.select({ desktop }))更廣泛的檢查:在以桌面慣例執行、位於瀏覽器自身視窗中的瀏覽器裡,它同樣成立。請以 platform.desktop 判斷桌面版面配置、密度與輸入方式,並以 isAvailable 判斷是否呼叫 Window 和 Menu。

#視窗

const Window: {
  setTitle(title: string): void;
  setTitleBar(options: { style: "default" | "hidden"; height?: number }): void;
  setMenuBar(mode: "shown" | "autoHide"): void;
  minimize(): void;
  toggleMaximize(): void;
  toggleFullScreen(): void;
  setFrame(frame: {
    origin?: Partial<Point>;
    size?: Partial<Size>;
    animated?: boolean;
  }): void;
  setLevel(level: "normal" | "floating" | "desktopIcon"): void;
  orderFront(): void;
  close(): void;
};

Rust 欄位列出 nsh_core::element::desktop 的函式。

成員Rust說明
setTitle(title)set_title(title)視窗的標題:顯示於標題列、工作列或 Dock、視窗切換器。
setTitleBar({ style, height? })set_title_bar(TitleBarStyle::Hidden, Some(px(height)))style: "hidden" 會移除系統的標題列:app 會填滿視窗直到頂端邊緣,並自行繪製標題列。系統的視窗控制項仍會保留(macOS 的紅綠燈按鈕、Windows 的最小化/最大化/關閉按鈕、GTK 的視窗按鈕)。height 是你的標題列高度,單位為 point:控制項會在其中置中(Windows 的標題按鈕會填滿該高度;在 macOS 上,height 會在 44 point 以下選用精簡樣式,超過則選用標準樣式;請使用 titleBar().height 提供的實際高度來設計你的標題列,而紅綠燈按鈕會由 AppKit 在即時調整大小時定位。嵌入式 app 既有的工具列會保留自己的樣式與高度);未設定時則採用平台的高度。style: "default" 會恢復系統的標題列(Rust:set_title_bar(TitleBarStyle::Default, None))。
setMenuBar(mode)set_menu_bar_visibility(mode)在 Windows 和 Linux 的 GPUI app 中,視窗會將應用程式選單繪製成 app 上方的選單列:"shown"(預設)會一律顯示;"autoHide" 則在單獨按下並放開 Alt 後顯示,直到再次單獨按下 Alt 才隱藏,與 Windows 和 GNOME 的 app 相同。無論哪種設定,F10 以及 Alt 加上選單的首字母都會開啟選單,且選單開啟期間選單列會顯示。macOS 的選單列由系統管理。
minimize()minimize()將視窗最小化。
toggleMaximize()toggle_maximize()將視窗最大化;若已最大化則還原。
toggleFullScreen()toggle_full_screen()進入或離開全螢幕。
setFrame(frame)set_frame(WindowFrame { .. })以 point 為單位移動視窗並調整大小;省略的欄位會保留原值。在 macOS 和 Windows 上,size(width、height)包含視窗裝飾;在 Linux 上則設定 GTK 的預設視窗大小。origin(x、y)是視窗的左上角,從主螢幕(在 macOS 上為有選單列的螢幕)的左上角起算,y 向下增加,各平台皆相同;只有 height 而沒有 y 時,上邊緣會維持原位,因此視窗會從底部邊緣放大或縮小。animated: true 會啟用 macOS 的調整大小動畫(預設:false)。
setLevel(level)set_level(level)在 macOS 和 Windows 上,"floating" 會讓視窗保持在一般視窗之上,"normal" 則恢復一般層級。"desktopIcon" 會選用 macOS 的桌面圖示層級(視窗也會出現在每個空間,但不會出現在 Mission Control 和 Cmd+` 中);在 Windows 上,它會將視窗移到目前 z 順序的最底部,之後的啟用動作可能會將它拉高。
orderFront()order_front()將視窗帶到其層級的最前方並取得焦點(app 會啟用;macOS 會取消最小化,Windows 會還原最小化的視窗並設為前景,GTK 會呈現視窗)。視窗會保持原本的層級。在 macOS 上,桌面層級的表面仍會位於其他視窗之後。GPUI 會啟用它的視窗;在 Linux 上,前景請求由視窗管理員控制。
close()close_window()關閉視窗;app 會隨之結束。

視窗大小調整支援 macOS、Windows 和 Linux;定位與層級則支援 macOS 和 Windows。Linux 的視窗管理員控制位置與堆疊順序。在 GPUI app 中,setFrame 會設定視窗大小並由平台決定位置;GPUI 的 host 會在開啟視窗時選擇其位置與層級。

依據以 onLayout 量測的內容來調整視窗大小時,請在 macOS 和 Windows 上為視窗裝飾預留空間。width 和 height 接受有限的正數;x 和 y 接受有限的數字。無效的欄位會保留先前的值。

#自訂標題列

隱藏標題列後,app 會自行繪製標題列,並透過 Rust 中的 .window_drag(true) 與 TSX 中的 desktop={{ windowDrag: true }} 指定其中哪些部分可以移動視窗:

use nsh_core::{
    desktop::TitleBarStyle,
    element::{desktop::{set_title_bar, title_bar}}, prelude::*,
};

#[component]
fn TitleBar() -> impl IntoElement {
    set_title_bar(TitleBarStyle::Hidden, Some(px(40.0))); // a bar 40 points tall
    view()
        .window_drag(true)
        .h(move || match title_bar().top {
            top if top > px(0.0) => top,
            _ => px(40.0),
        })
        .pl(move || title_bar().left + px(8.0)) // macOS's traffic lights
        .pr(move || title_bar().right + px(8.0)) // the caption buttons on Windows, GTK's buttons
        .flex_row()
        .items_center()
        .child(text("My App"))
        .child(view().flex_1())
        .child(input().placeholder("Search").w(px(180.0)))
}

按下設有 windowDrag 的 view 會像系統的標題列一樣拖曳視窗:按兩下會將視窗最大化(或依使用者的設定執行,在 macOS 上為「按兩下視窗的標題列以」),而在 Windows 和 Linux 上,按右鍵會開啟視窗選單。

  • 由最近的、設定了 windowDrag 的 view 決定:.window_drag(false)(desktop={{ windowDrag: false }})可讓可拖曳 view 中的某個部分保持可點擊。
  • 可拖曳 view 內的文字欄位以及帶有按下或觸控處理常式(onPress、onTouch*、responder)的 view,即使沒有它也能保持可點擊:上例中的搜尋欄位和所有按鈕都能照常運作。
  • windowDrag 也適用於系統的標題列:側邊欄或工具列同樣可以用來移動視窗。

#titleBar

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

隱藏標題列的區域,單位為 point(reactive):包含它的 height,以及系統視窗控制項在其 left 與 right 兩端所佔的寬度。請依此配置你的標題列,避免內容位於控制項之下。當系統顯示其標題列時(以及在全螢幕和非桌面環境中),這三個值皆為 0。Rust:title_bar() -> Edges<Points>,其中 top 為高度,left 和 right 為控制項的寬度;讀取它會訂閱其變更。

heightleftright
macOSheight,否則為標題列的高度(28 或 32,視 macOS 版本而定)紅綠燈按鈕的寬度0
Windowsheight,否則為 320三個標題按鈕(138)
Linuxheight,否則為標頭列的高度起始端的視窗按鈕(依桌面環境的按鈕配置)結尾端的視窗按鈕

在 Windows 上,標題按鈕由 NSH 以 Windows 11 的樣式繪製並跟隨 app 的外觀;將滑鼠移到最大化按鈕上會顯示 Windows 11 的貼齊版面配置。在每個平台上,視窗都會保留其調整大小的邊框、陰影和圓角。

#選單

const Menu: {
  setApplicationMenu(items: MenuItem[] | null): void;
  popup(items: MenuItem[], at?: Point): Promise<void>;
};
成員說明
setApplicationMenu(items)設定應用程式選單:每個頂層項目對應一個子選單(File、Edit、…)。在 macOS 上它是選單列,第一個選單為 app 選單(AppKit 會以 app 的名稱作為其標題)。在 Windows 和 Linux 上它是視窗的選單列,隱藏標題列時不會顯示;其快速鍵仍可運作,而 app 自訂的標題列可以用 Menu.popup 開啟它的選單。null 會恢復平台的預設選單。
popup(items, at?)顯示右鍵選單,其左上角位於 at(視窗座標,單位為 point:例如 onContextMenu 的 e.pagePosition),或位於指標處。此 promise 會在選單關閉後 resolve,無論是否選擇了項目:開啟選單的按鈕可以在此之前保持按下狀態。

在 Rust 中,set_application_menu(items) 接受 Vec<MenuItem>(None 會恢復平台的預設選單),而 popup_menu(items)(位於指標處)和 popup_menu_at(items, at) 會回傳一個 Answer<()>,在選單關閉後完成(是否 await 它為選擇性)。項目的處理常式會在設定該選單的 owner 底下執行,直到該 owner 結束。

再次呼叫 setApplicationMenu 即可變更選單。在 effect 內呼叫時,它會追蹤所讀取的 signal:依 app 狀態而定的勾選記號或停用狀態會保持最新。

#選單項目

type MenuItem =
  | { type: "separator" }
  | {
      label?: string;
      role?: MenuRole;
      shortcut?: string;
      when?: "always" | "auto" | "notEditing";
      disabled?: 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+?")會擲出錯誤並提示正確的寫法。keymap 接受相同的按鍵組合。選單會以平台自身的樣式顯示它(macOS 上為 ⇧⌘N,Windows 上為 Ctrl+Shift+N)。
when在 GPUI 後端上,當文字欄位擁有鍵盤焦點時,快速鍵何時會選取該項目:"always"(預設)、"auto"(除非欄位本身會使用該按鍵:包括會輸入文字、移動插入點或刪除的按鍵,以及編輯快速鍵,如同 keymap 的綁定)或 "notEditing"。項目放過的按鍵會交給欄位處理。
disabledtrue 會將項目設為灰色(預設為 false)。
checked設為(true 或 false)時,項目會帶有勾選記號。
submenu使項目成為子選單(需要搭配 label)。
role由平台執行的標準項目(見下文)。

null、undefined 和 false 的項目會被略過,因此 cond && { … } 可以在清單中使用。

在 Rust 中,項目為 MenuItem::new(label)、MenuItem::role(role)、MenuItem::separator() 或 MenuItem::submenu(label, items),各欄位對應為方法:.shortcut("mod+n")(快速鍵無效時會 panic,並提示正確的寫法)、.shortcut_when(When::Auto)、.disabled(true)、.checked(on)、.on_select(f),以及用於具有 role 項目的 .label(..)。

修飾鍵:

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

app 自己的快速鍵請使用 mod:它是各平台 app 用於此目的的按鍵(macOS 上為 ⌘N,Windows 和 Linux 上為 Ctrl+N),因此一種寫法即可適用於所有平台。Windows 鍵和 Super 大多由系統使用(Win+E、GNOME 概覽),所以 cmd 在 macOS 以外很少是合適的選擇。

Role:

Role作用平台
about、hide、hideOthers、showAllapp 選單的標準項目macOS(其他平台會略過)
quit結束 app所有平台
undo、redo、cut、copy、paste、selectAll編輯取得焦點的文字欄位(其自身的快速鍵仍由它自己處理)所有平台
minimize、zoom、toggleFullScreen、close視窗所有平台

除非自行設定,具有 role 的項目會使用平台的標籤與快速鍵(macOS 會在 about、hide 和 quit 加上 app 的名稱)。

選單快速鍵優先,與原生 app 相同:項目佔用的按鍵絕不會傳到取得焦點的文字欄位或 NSH 本身的按鍵。只有具有相同按鍵組合的 keymap 綁定會排在它們之前。因此,編輯快速鍵(mod+c、mod+v、mod+x、mod+a、mod+z)只應指派給編輯 role,由它們傳遞給文字欄位;並請避免使用沒有修飾鍵的按鍵(escape、方向鍵、字母),否則會從文字欄位和返回導覽中奪走這些按鍵。在 GPUI 後端上,when: "auto" 可讓項目與文字欄位共用同一個按鍵:delete 或 mod+z 會在沒有欄位取得焦點時選取該項目,而在有欄位取得焦點時編輯文字。

在 GPUI 後端上,取得鍵盤焦點的原生元件會最先接收每個按鍵。它自己處理的按鍵絕不會傳到 keymap 或選單。它放過的按鍵會像有文字欄位取得焦點一樣傳到 keymap,然後才傳到選單。

use nsh_core::{
    desktop::MenuRole,
    element::desktop::{MenuItem, set_application_menu},
    platform::Os,
    prelude::*,
};

#[component]
fn AppMenu() -> impl IntoElement {
    let mac = os() == Os::MacOs;
    let wrap = signal(true);
    effect(move || {
        let mut menus = Vec::new();
        if mac {
            menus.push(MenuItem::submenu("My App", [
                MenuItem::role(MenuRole::About),
                MenuItem::separator(),
                MenuItem::role(MenuRole::Hide),
                MenuItem::role(MenuRole::HideOthers),
                MenuItem::separator(),
                MenuItem::role(MenuRole::Quit),
            ]));
        }
        let mut file = vec![
            MenuItem::new("New").shortcut("mod+n").on_select(create_document),
            MenuItem::new("Open…").shortcut("mod+o").on_select(open_document),
        ];
        if !mac {
            file.push(MenuItem::separator());
            file.push(MenuItem::role(MenuRole::Quit).label("Exit"));
        }
        menus.push(MenuItem::submenu("File", file));
        menus.push(MenuItem::submenu("Edit", [
            MenuItem::role(MenuRole::Undo),
            MenuItem::role(MenuRole::Redo),
            MenuItem::separator(),
            MenuItem::role(MenuRole::Cut),
            MenuItem::role(MenuRole::Copy),
            MenuItem::role(MenuRole::Paste),
        ]));
        menus.push(MenuItem::submenu("View", [
            MenuItem::new("Word Wrap")
                .checked(wrap.get())
                .on_select(move || wrap.update(|w| *w = !*w)),
            MenuItem::role(MenuRole::ToggleFullScreen),
        ]));
        set_application_menu(menus);
    });
    view()
}

由按鈕按下所開啟的右鍵選單:

text("More").on_press(|| {
    let _ = popup_menu(vec![
        MenuItem::new("Rename").on_select(rename),
        MenuItem::new("Duplicate").on_select(duplicate),
        MenuItem::separator(),
        MenuItem::new("Delete").on_select(remove),
    ]);
})