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

元素

Dock

停靠工作區是由使用者自行編排的工作空間:面板放在分頁群組中,群組可並排或上下堆疊分割,而左、右、下三邊的停靠欄可以開啟、關閉及調整大小。把分頁拖到另一個群組,就會加入該群組;拖到群組邊緣,就會分割該群組;面板移動時會保留其內容。DockArea 負責行為:分頁的拖放(以原生方式逐幀執行)、調整分割及停靠欄的大小、開啟和關閉停靠欄、縮放與鎖定。外觀則由應用程式決定:分頁、分頁列、拖曳把手、放置指示器及各面板的內容,都由應用程式渲染並設定樣式,工作區本身不繪製任何內容。

el-dock.tsx
el-dock.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Button, DockArea, type DockAreaLayout, type DockAreaRef } from "nsh/base";
import { button, buttonText, desktop, focusRing, Log, Panel, Split, ui, wide } from "./palette";

// The panels' keys and what each shows. The layout lists the keys; the app
// renders a panel's content from its key, and the content moves with it.
const titles: Record<string, string> = {
  files: "Files",
  readme: "README.md",
  main: "main.rs",
  preview: "Preview",
  terminal: "Terminal",
  problems: "Problems",
};
const lines: Record<string, string[]> = {
  files: ["src/", "  main.rs", "  lib.rs", "README.md", "Cargo.toml"],
  readme: ["# Hopper", "", "A small app with a dockable workspace."],
  main: ["fn main() {", "    hopper::run();", "}"],
  preview: ["Hopper", "A small app with a dockable workspace."],
  terminal: ["$ cargo run", "   Compiling hopper v0.1.0", "    Finished dev in 1.92s"],
  problems: ["No problems"],
};

const initial: DockAreaLayout = {
  center: {
    type: "split",
    axis: "horizontal",
    children: [
      { type: "tabs", panels: ["readme", "main"], activeIndex: 1 },
      { type: "tabs", panels: ["preview"] },
    ],
    sizes: [null, 260],
  },
  left: { layout: { type: "tabs", panels: ["files"] }, size: 180 },
  bottom: { layout: { type: "tabs", panels: ["terminal", "problems"] }, size: 130 },
};

function App() {
  const layout = signal<DockAreaLayout>(initial);
  const locked = signal(false);
  // What shows now, in either presentation: the layout's docks when the area
  // is wide, the drawer or sheet presented when it is narrow. It starts from
  // the layout and follows onDockOpenChange.
  const shown = signal<Record<string, boolean>>({ left: true, bottom: true });
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  let area!: DockAreaRef;

  const toolbarButton = (label: string, onPress: () => void, on?: () => boolean) => (
    <Button onPress={onPress} selected={on?.() ?? false} {...button("secondary", { size: "sm", on })}>
      <Text style={buttonText("secondary", { on })}>{label}</Text>
    </Button>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>dock</Text>
      <Text style={{ color: ui().muted }}>
        {desktop
          ? "Drag a tab onto another group, or to a group's edge to split it"
          : "Tap Files or Terminal to open a dock; hold a tab to reorder it"}
      </Text>

      <Split
        grow
        width={720}
        inspector={
          <>
            <Panel title="docks">
              <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
                {toolbarButton("Files", () => area.toggleDock("left"), () => shown().left)}
                {toolbarButton("Terminal", () => area.toggleDock("bottom"), () => shown().bottom)}
                {toolbarButton("Lock", () => locked.set(!locked()), locked)}
                {toolbarButton("Reset", () => layout.set(initial))}
              </View>
            </Panel>
            <Log title="events" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 420 : 460, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <DockArea
            ref={area}
            class="flex-1"
            layout={layout()}
            onLayoutChange={({ layout: next }) => {
              layout.set(next);
              log("onLayoutChange");
            }}
            onDockOpenChange={({ placement, open }) => {
              shown.set({ ...shown(), [placement]: open });
              log(`${placement} ${open ? "shown" : "hidden"}`);
            }}
            onPanelActiveChange={({ panel, active }) => active && log(`${titles[panel]} shown`)}
            locked={locked()}
            // The demo's two layouts: the full workspace beside the inspector in a
            // wide window, the phone presentation in one column. Without `compact`
            // the area decides by its own width (under 600 points: compact).
            compact={!wide()}
            groupClass="flex-1"
            tabBarClass="flex-row border-b border-border"
            tabClass="group px-3 py-1.5 selected:bg-muted"
            contentClass="flex-1"
            handleClass="bg-border group-hover:bg-muted-foreground group-active:bg-primary"
            handleAccessibilityLabel="Resize"
            dockClass="bg-muted/40"
            dropIndicatorClass="rounded-(--radius) border-2 border-primary bg-primary/10"
            drawerClass="bg-background border-r border-border"
            sheetClass="rounded-t-(--radius-lg) bg-background"
            backdropClass="bg-black/30"
            renderTab={(tab) => (
              // One view inside the tab: a row holding the title and the close button.
              <View class="flex-row items-center gap-1.5">
                <Text class="text-[13px] text-muted-foreground group-selected:text-foreground">{titles[tab.panel]}</Text>
                {tab.closable && desktop ? (
                  <Button
                    onPress={() => area.closePanel(tab.panel)}
                    accessibilityLabel={`Close ${titles[tab.panel]}`}
                    class="rounded-sm px-1 hover:bg-muted active:opacity-70"
                    focusStyle={focusRing()}
                  >
                    <Text class="text-xs text-muted-foreground">×</Text>
                  </Button>
                ) : null}
              </View>
            )}
            renderDragPreview={(tab) => (
              <View class="rounded-(--radius) border border-border bg-background px-3 py-1.5 shadow-md">
                <Text class="text-[13px] text-foreground">{titles[tab.panel]}</Text>
              </View>
            )}
            renderPanel={(panel) => (
              <View class="flex-1 gap-1 p-3">
                {(lines[panel.id] ?? []).map((line) => (
                  <Text class="font-mono text-xs text-foreground">{line || " "}</Text>
                ))}
              </View>
            )}
            renderEmpty={() => (
              <View class="flex-1 items-center justify-center">
                <Text class="text-sm text-muted-foreground">Every panel is closed</Text>
              </View>
            )}
          />
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{
    dock::{DockAreaLayout, DockLayout, DockPanel, DockPlacement, DockRegion, DockTab},
    dock_area, theme,
};
use nsh_core::prelude::*;

fn workspace() -> impl IntoElement {
    let layout = signal(
        DockAreaLayout::new(
            DockLayout::h_split()
                .child(DockLayout::tabs().panel("readme").panel("main"), None)
                .child(DockLayout::tabs().panel("preview"), Some(px(320.0))),
        )
        .with_dock(DockPlacement::Left, DockRegion::new(DockLayout::tabs().panel("files")))
        .with_dock(DockPlacement::Bottom, DockRegion::new(DockLayout::tabs().panel("terminal"))),
    );
    dock_area()
        .layout(move || layout.get())
        .on_layout_change(move |next: &DockAreaLayout| layout.set(next.clone()))
        .handle_style(|s| s.bg(move || theme().border()))
        .tab_bar_style(|s| s.flex_row().border_b_1().border_color(move || theme().border()))
        .tab_style(|s| s.px_3().py_1().selected(|s| s.bg(move || theme().muted())))
        .render_tab(|tab: &DockTab| text(tab.panel.to_string()).text_sm().text_color(move || theme().foreground()))
        .render_panel(|panel: &DockPanel| {
            text(format!("{} content", panel.id)).p_4().text_color(move || theme().foreground())
        })
}

DockArea 及其接受的值來自 nsh/base,即 Rust 的 nsh-base crate(nsh_base::dock_area(),值在 nsh_base::dock 中)。在 Rust 中,由 dock_area() 建立,每個屬性有一個 setter,每個事件有一個監聽器,並提供具型別的樣式方法;DockLayout 以面板的 key 描述版面配置(h_split()、v_split()、tabs()、.child(layout, size)、.panel(key))。

#版面配置

版面配置是一個值,即 DockAreaLayout:包含中央區域,以及左、右、下三邊各一個停靠欄,每個都是可選的。每個區域都是由兩種節點組成的樹:

節點 內容 欄位
{ type: "split" } 其他節點,並排(axis: "horizontal")或上下堆疊("vertical") children,以及 sizes:每個子節點沿軸向的長度(以點為單位),null 表示平分餘下空間
{ type: "tabs" } 面板(以 key 表示),一次只顯示一個 panels(key,依分頁順序)、activeIndex(顯示中的一個,預設為 0)

面板一定位於某個分頁群組中:只有一個面板的區域,就是只含一個分頁的群組。面板的 key 是應用程式為它指定的字串("files"、文件的 id),而且每個 key 在整個版面配置中只能出現一次。停靠欄(DockRegion)包含它的樹(layout)、橫跨其邊緣的大小(size,預設為 200,最小為 100)、是否開啟(open,預設為 true),以及將其邊緣拖到小於最小尺寸時是否會關閉它(collapsible,預設為 true)。

每個節點都有 id。應用程式寫入的版面配置可以省略 id:工作區會自動指派,而它回報的每個版面配置都帶有 id。將回報的版面配置傳回去,可保留每個群組的身分,ref 的方法正是以此來指定群組。工作區也會整理收到的內容:空的群組或分割會被移除,只有一個子節點的分割會變成該子節點,位於同軸向分割內的分割會併入其父節點,activeIndex 會保持在有效範圍內,重複列出的 key 只保留第一個位置。它回報的版面配置一律採用這種標準形式。

版面配置由應用程式持有(layout):使用者所做的每項更改(選取、拖動或關閉分頁,調整分割或停靠欄大小,開啟或關閉停靠欄)都會以完整的新版面配置回報(onLayoutChange),而工作區會顯示應用程式傳回的版面配置。應用程式透過傳入新值來自行更改版面配置,例如還原已儲存的版面配置,或執行重新編排工作空間的命令。沒有 layout 的工作區會自行維護版面配置,從 defaultLayout 開始,並仍會回報每項更改。

#面板內容

面板顯示的內容由應用程式決定:renderPanel 根據面板的 key 渲染內容(panel.id)。群組中的每個面板都會被配置位置,但只有顯示中的面板可見,因此切換分頁時,每個面板的內容都會保留;而當面板移動(到另一個群組、移入停靠欄、進入縮放再返回)時,相同的內容會隨之移動:它建立的 signal、輸入框的文字、捲動位置、web view 的頁面都會保留。工作區只會在面板首次出現時渲染一次,並在面板離開版面配置時釋放其內容(onPanelRemove)。

<DockArea
  layout={layout()}
  onLayoutChange={(e) => layout.set(e.layout)}
  renderPanel={(panel) => (panel.id === "terminal" ? <Terminal /> : <Editor file={panel.id} />)}
  onPanelActiveChange={({ panel, active }) => active && setTitle(panel)}
  onPanelRemove={({ panel }) => closeFile(panel)}
/>

panels 以 key 設定每個面板的許可;未列出的面板允許所有操作:

欄位 預設值 說明
id 面板的 key。
closable true 可以關閉。
zoomable true 顯示期間,其所屬群組可以縮放。
visible true 是否顯示。隱藏的面板會保留其位置,並在重新顯示時回到原處;所有面板均被隱藏的群組或分割不佔用空間。

#分頁

每個群組都有一個分頁列,每個可見面板對應一個分頁,順序依照 panels。renderTab 渲染分頁所顯示的內容(標題、圖示、應用程式自訂的關閉按鈕);未提供時,分頁會顯示面板的 key。工作區會把它作為分頁處理:按下即選取,拖曳即移動其面板,螢幕閱讀器會將它讀作帶有選取狀態的分頁,而 tabClass 的 selected: class 會為顯示中的分頁設定樣式。分頁的值說明了應用程式繪製它所需的資料:

欄位 型別 說明
panel string 面板的 key。
group number 它所在分頁群組的 id。
index number 它在群組可見分頁中的位置。
active boolean 它是群組中顯示中的分頁。
closable boolean 工作區可以關閉它:它可關閉、工作區未鎖定,而且其群組可以放棄它。
zoomed boolean 其群組已縮放。
let area!: DockAreaRef;

<DockArea
  ref={area}
  layout={layout()}
  onLayoutChange={(e) => layout.set(e.layout)}
  renderTab={(tab) => (
    <View class="flex-row items-center gap-2">
      <Text class="text-sm">{titles[tab.panel]}</Text>
      <If when={tab.closable}>
        <Button onPress={() => area.closePanel(tab.panel)} accessibilityLabel="Close" class="rounded-sm px-1 hover:bg-muted">
          <Text class="text-xs">×</Text>
        </Button>
      </If>
    </View>
  )}
  renderPanel={(panel) => <Editor file={panel.id} />}
/>

#拖放

將分頁拖過群組時,會顯示它將落在何處,放開後就會放到該處:

放置位置 面板
分頁列 加入該群組,位於放開處兩旁的分頁之間
群組內容的中間 加入該群組
群組內容的邊緣(每側外圍 35%) 分割該群組:在該側建立一個新的獨立群組

拖曳懸停期間,工作區會在面板將佔用的群組部分上方顯示放置指示器(加入時覆蓋全部內容,分割時覆蓋一半):這是一個由應用程式設定樣式的部分(dropIndicatorClass),如果應用程式渲染了自己的內容(renderDropIndicator,會收到 group、它所分割出的 side 或分頁列 index),就會顯示該內容。它會以原生方式移動到每個新位置,並帶有緩動效果;移到另一個群組時,會在前一個消退的同時淡入(當使用者要求減少動態效果時則立即切換)。跟隨指標或手指的內容同樣由應用程式決定(renderDragPreview)。一次放置只算一次版面配置更改,只會回報一次;不會造成任何改變的放置,則不會更改任何內容。

已鎖定的工作區、已縮放的工作區,以及旁邊沒有其他內容的群組中的最後一個面板,都不能拖曳。

#停靠欄

停靠欄開啟和關閉時,不會失去其面板或大小:關閉時會保留它的樹,再開啟時會以相同大小顯示相同的群組。停靠欄與中央區域之間的邊緣是一個把手,使用者可拖曳它來調整停靠欄大小;停靠欄至少保持 100 點,中央區域也至少保持 100 點,而可收合的停靠欄若在小於 50 點時放開,就會關閉,並保留其大小供下次開啟時使用。工作區一定會配合其邊界:停靠欄會以其 size 顯示,但受限於工作區留給它的空間;若連這樣也放不下,停靠欄會與其餘部分一同縮小,而分割的最小尺寸也會讓步。renderDock 會在停靠欄的版面配置之前繪製它自己的外框內容(標題列、收合按鈕),並會收到它的 placement、open、size 和 collapsible;dockClass 為其外框設定樣式。

應用程式可透過 ref 開啟、關閉及調整停靠欄大小(toggleDock、setDockOpen、setDockSize),這些方法在兩種呈現方式下都有效;也可以在傳入的版面配置中寫入停靠欄的 open 和 size(open 屬於完整呈現方式)。每當停靠欄顯示或隱藏時,onDockOpenChange 都會回報,並以版面配置的 open 為基準來衡量(一開始就是精簡呈現的工作區,會將每個在版面配置中為開啟的停靠欄回報為隱藏),因此從版面配置的 open 開始並跟隨 onDockOpenChange 的切換,總能顯示停靠欄的狀態:

const terminalOpen = signal(true);

<DockArea
  ref={area}
  layout={layout()}
  onLayoutChange={(e) => layout.set(e.layout)}
  onDockOpenChange={({ placement, open }) => placement === "bottom" && terminalOpen.set(open)}
  renderPanel={(panel) => <Editor file={panel.id} />}
/>

<Button selected={terminalOpen()} onPress={() => area.toggleDock("bottom")} class="px-3 py-1.5 selected:bg-muted">
  <Text>Terminal</Text>
</Button>

#縮放

已縮放的群組會填滿整個工作區,直至被縮小或離開版面配置為止(area.toggleZoom(group),以及用於縮小的 area.toggleZoom()),而 onZoomChange 會回報它所顯示的面板,或 null。縮放屬於群組,因此當其顯示中的面板關閉、由下一個面板接替時,群組仍保持縮放。設有 zoomable: false 的面板在顯示期間,會使其群組無法縮放。

#鎖定

locked 會讓編排保持原樣:不能拖曳分頁、不能放置、不能關閉。選取分頁、調整大小,以及開啟和關閉停靠欄仍然可用,因此已鎖定的工作空間依然可以使用。

#方法

ref={area} 提供工作區的方法(DockAreaRef),供應用程式的命令、快速鍵和選單使用。每項編輯都會像使用者的操作一樣,以 onLayoutChange 回報。如果傳給方法的面板、群組、分頁索引或停靠欄不存在於版面配置中,版面配置會維持原狀,並記錄一則指出方法名稱和該值的警告。

方法 說明
layout() Promise<DockAreaLayout>:目前顯示的版面配置。
addPanel(panel, placement?, size?) 將面板加入某個區域("center",即預設值、"left"、"right"、"bottom"),放入其第一個分頁群組並設為顯示中;若停靠欄不存在則會建立,橫跨寬度為 size。工作區已有的面板則會移到該處。
removePanel(panel) 移除面板,不論其選項如何設定。
closePanel(panel) 以分頁關閉的方式關閉面板:若工作區已鎖定、面板不可關閉,或它是旁邊沒有其他內容的群組中的最後一個面板,則不會關閉。
movePanel(panel, target) 將面板移到 target:{ group, side?, index?, activate?, size? }、移入分頁群組 group(位於 index),或移到其 side 的新群組(size 為沿新分割的點數)。
splitAt(group, panel, side) 將面板放入容器某一側的新分頁群組("left"、"right"、"top"、"bottom")。
selectPanel(panel) 在其群組中顯示某個面板。
selectTab(group, index) 顯示群組的某個分頁。
toggleDock(placement)、setDockOpen(placement, open) 開啟或關閉停靠欄。
setDockSize(placement, size) 設定停靠欄橫跨其邊緣的大小(至少 100)。
toggleZoom(group?) 縮放群組;若群組已縮放則縮小;沒有 group 時,則縮小已縮放的群組。
zoomedGroup() Promise<number | null>:已縮放群組的 id。
groupOf(panel) Promise<number | null>:容納某個面板的分頁群組 id。

在 Rust 中,工作區上的 component_ref::<DockArea>() 和 .component_ref(&r) 會提供一個 ref,其 r.api::<DockAreaApi>() 具有相同的方法,名稱採用 snake case。

#儲存與還原

回報的版面配置是純資料,因此儲存只需對它執行 JSON.stringify(layout) 並寫入儲存空間,而還原則是將解析後的值作為 layout 傳入。onLayoutChange 會回報每項更改,包括調整大小和選取分頁,因此請在更改穩定後才寫入儲存空間(使用在每次回報時重設的短計時器)。

在 Rust 中,DockAreaLayout::to_state 和 from_state 讀寫 DockAreaState,這是 gpui-base 的停靠欄同樣使用的版面配置檔案格式。

#手機與窄視窗

停靠欄會配合自身的寬度。寬度達 600 點或以上的工作區(在桌面視窗、網頁、平板、全螢幕 iPad、展開的摺疊式裝置中)是完整的工作空間:分割、群組和停靠欄並排顯示,一如平板上的多窗格應用程式。在手機或平板上用手指操作時,分頁拖曳會在長按後開始,與 iPadOS 的拖放一樣,因此分頁列仍可透過滑動捲動,而把手會有更闊的觸控範圍,外觀則維持不變。

寬度少於 600 點的工作區(在手機、Slide Over 中的 iPad、窄的桌面或瀏覽器視窗,或寬版面中的窄欄內)會為單一畫面呈現相同的版面配置,就如 iPad 的分割檢視會收合其欄位,而 Material Design 的輔助窗格會變成底部面板:

  • 中央區域一次以完整大小顯示一個群組。它的分頁列會列出中央區域的每個可見面板,選取另一個群組的面板,就會讓該群組成為顯示中的群組。
  • 左右停靠欄是從邊緣覆蓋在中央區域上的抽屜,寬度與停靠欄相同;下方停靠欄則是高度與停靠欄相同的底部面板。同一時間只會顯示一個,由應用程式呈現時才會出現(toggleDock、setDockOpen,會令另一個被關閉);它會滑入和滑出,按下背景遮罩、按 Escape 或返回,都會將它關閉。進入精簡呈現時,不會顯示任何一個。
  • 拖曳分頁(手指在長按後,或指標立即拖曳)時,會在分頁列內移動以重新排序。

兩種呈現方式下,每個面板都保持已配置位置,因此切換時其內容會保留。所顯示的抽屜或底部面板屬於精簡呈現本身:版面配置的 open 屬於完整呈現,而呈現或關閉抽屜不會回報 onLayoutChange,因此再次變寬的工作區會原樣顯示停靠欄。onDockOpenChange 會回報兩種情況:在完整呈現中 open 改變的停靠欄、在精簡呈現中顯示或關閉的抽屜或底部面板,以及呈現方式切換時顯示狀態改變的每個停靠欄。在 renderDock 中,open 表示該停靠欄在目前的呈現方式下是否顯示。版面配置也仍是同一個值,因此當工作區跨越 600 點(摺疊式裝置展開、iPad 加入 Split View、視窗或欄位被調整大小)時,每個面板都會留在原處,應用程式也只需儲存一份版面配置。工作區完成版面配置後,由其寬度決定呈現方式(在此之前以視窗寬度代替),而 compact 會取代寬度來選擇呈現方式。抽屜、底部面板和背景遮罩都是由應用程式設定樣式的部分(drawerClass、sheetClass、backdropClass),與工作區其餘部分相同。

<DockArea
  layout={layout()}
  onLayoutChange={(e) => layout.set(e.layout)}
  drawerClass="bg-background border-r border-border"
  sheetClass="rounded-t-(--radius-lg) bg-background"
  backdropClass="bg-black/40"
  renderTab={(tab) => <Text>{titles[tab.panel]}</Text>}
  renderPanel={(panel) => <Editor file={panel.id} />}
/>

#屬性

屬性 Rust 類型 預設值 說明
layout .layout(..) DockAreaLayout 無 顯示的版面配置(受控):傳回 onLayoutChange 所報告的內容。
defaultLayout .default_layout(..) DockAreaLayout 空白的中央區域 起始版面配置,在未設定 layout 時使用;此後區域自行維持其版面配置。
locked .locked(..) boolean false 不可拖曳、放置或關閉分頁。
compact .compact(..) boolean 按寬度 手機呈現方式(true)或完整呈現方式(false);未設定時,區域窄於 600 點便採用精簡呈現方式。
panels .panels(..) DockPanelOptions[] [] 各面板允許的操作,按鍵值設定:closable、zoomable、visible。
renderPanel .render_panel(|panel| ..) (panel: DockPanel) => JSX.Element 無 面板的內容,按其鍵值(panel.id)設定。
renderTab .render_tab(|tab| ..) (tab: DockTab) => JSX.Element 鍵值 分頁顯示的內容。
renderDropIndicator .render_drop_indicator(|drop| ..) (drop: DockDropIndicator) => JSX.Element 無 放置指示器內顯示的內容:group、side、index。
renderDock .render_dock(|dock| ..) (dock: DockInfo) => JSX.Element 無 停靠區本身的外框,位於其版面配置之前:placement、open(於目前呈現方式中顯示)、size、collapsible。
renderEmpty .render_empty(|placement| ..) (placement: DockPlacement) => JSX.Element 無 空白中央區域顯示的內容。
renderDragPreview .render_drag_preview(|tab| ..) (tab: DockTab) => JSX.Element 無 拖曳分頁時跟隨指標或手指的內容。
groupClass、groupStyle .group_style(|s| ..) string、StyleProp<ViewStyle> 無 每個分頁群組的外框。
tabBarClass、tabBarStyle .tab_bar_style(|s| ..) string、StyleProp<ViewStyle> 無 每個分頁列(flex-row 用於單行)。
tabClass、tabStyle .tab_style(|s| ..) string、StyleProp<ViewStyle> 無 每個分頁;selected: 用於正在顯示的分頁。
contentClass、contentStyle .content_style(|s| ..) string、StyleProp<ViewStyle> 無 每個群組的內容外框,面板在此顯示。
dropIndicatorClass、dropIndicatorStyle .drop_indicator_style(|s| ..) string、StyleProp<ViewStyle> 無 放置指示器。
handleClass、handleStyle .handle_style(|s| ..) string、StyleProp<ViewStyle> 無 每個把手的指示器,位於分割的群組之間及每個停靠區的邊緣;group-hover: 和 group-active: 跟隨把手。它橫跨把手寬 1 點,除非另有指定,否則沿整個長度延伸,所以 bg-border 無論哪個方向都是一條細線;w-[3px] / h-[3px] 可將它加粗。
handleThumbClass、handleThumbStyle .handle_thumb_class(..)、.handle_thumb_style(|s| ..) string、StyleProp<ViewStyle> 無 每個把手的滑鈕:置中於線上的抓握部件,在垂直線上直立繪製,在水平線上則旋轉四分之一圈,因此同一個 class 可用於兩個軸向。把手在懸停、按下和拖曳期間,滑鈕始終是同一個視圖,所以 group-hover: 和 group-active: class 會為它加上過渡效果(h-11 w-[3px] rounded-full bg-muted-foreground opacity-0 scale-y-0 transition group-hover:opacity-40 group-hover:scale-y-[0.45] group-active:opacity-90 group-active:scale-y-100)。
dockClass、dockStyle .dock_style(|s| ..) string、StyleProp<ViewStyle> 無 每個停靠區的外框。
drawerClass、drawerStyle .drawer_style(|s| ..) string、StyleProp<ViewStyle> 無 精簡模式:兩側停靠區的抽屜。
sheetClass、sheetStyle .sheet_style(|s| ..) string、StyleProp<ViewStyle> 無 精簡模式:底部停靠區的彈層。
backdropClass、backdropStyle .backdrop_style(|s| ..) string、StyleProp<ViewStyle> 無 精簡模式:已開啟的抽屜或彈層後方的背景遮罩,按下即會關閉抽屜或彈層。
handleAccessibilityLabel .handle_accessibility_label(..) string 無 螢幕閱讀器為每個把手朗讀的內容,包括分割的群組之間及每個停靠區邊緣的把手。
ref .component_ref(&r)(component_ref::<DockArea>()) DockAreaRef 區域的方法。
class、style 樣式方法 string、StyleProp<ViewStyle> 無 區域本身:其在父元件中的大小(flex-1)及背景。它保持在自身邊界內,不會從內容取得最小尺寸。

#事件

事件 Rust 說明
onLayoutChange .on_layout_change(|layout: &DockAreaLayout| ..) 版面配置已變更(拖曳、選取、調整大小、停靠區開啟或關閉、呼叫方法):{ layout },即整個新版面配置,包括其中每個 id。
onPanelActiveChange .on_panel_active_change(|e: &DockAreaEvent| ..)(PanelActiveChange(panel, active)) { panel, active }:某個面板成為或不再是其群組所顯示的分頁(已關閉的停靠區不顯示任何分頁)。每次變更只觸發一次;離開的面板不會收到 false:onPanelRemove 便是它的終結。
onPanelRemove .on_panel_remove(|panel: &PanelId| ..) { panel }:面板永久離開區域:已關閉、已移除,或在新版面配置中缺失。僅是移動位置的面板不會收到此事件。
onZoomChange .on_zoom_change(|panel: &Option<PanelId>| ..) 群組放大或縮小:{ panel }、其所顯示的面板,或 null。
onDockOpenChange .on_dock_open_change(|e: &DockAreaEvent| ..)(DockOpenChange(placement, open)) { placement, open }:停靠區顯示或隱藏:在完整呈現方式中其版面配置 open 已變更,在精簡呈現方式中其抽屜或彈層已顯示或收起,或呈現方式已切換。

#部件

部件 位於 樣式設定 狀態
區域 class、style
分頁群組 一個範圍 groupClass、groupStyle
分頁列 一個群組 tabBarClass、tabBarStyle
分頁 一個分頁列 tabClass、tabStyle;renderTab selected:、active:、hover:
群組的內容 一個群組 contentClass、contentStyle;renderPanel
把手的指示器 分割的群組之間、停靠區的邊緣 handleClass、handleStyle group-hover:、group-active:
把手的滑鈕 置中於把手的線上 handleThumbClass、handleThumbStyle group-hover:、group-active:(按下狀態持續至拖曳結束)
停靠區 一個邊緣 dockClass、dockStyle;renderDock
放置指示器 拖曳期間位於群組之上 dropIndicatorClass、dropIndicatorStyle;renderDropIndicator
拖曳預覽 指標或手指之下 renderDragPreview
空白中央區域 中央 renderEmpty
抽屜、彈層、背景遮罩 精簡模式下位於中央之上 drawerClass、sheetClass、backdropClass(及其樣式)

#無障礙

  • 對螢幕閱讀器而言,每個分頁都是帶有選取狀態的分頁,與 Tab 相同,啟用它即會選取該分頁。其標籤是 renderTab 繪製的文字。
  • 每個把手,包括分割的群組之間及每個停靠區邊緣的把手,都是可調整的元素,標籤為 handleAccessibilityLabel。分割的群組之間的把手屬於 Resizable:螢幕閱讀器每次移動 10 點,並以 onLayoutChange 報告。
  • 在精簡呈現方式下,按 Escape 和返回會收起所顯示的抽屜或彈層。

有關標籤、角色和播報的一般說明,請參閱無障礙。

#平台備註

  • 在所有平台上,指標(滑鼠、觸控板、懸停於螢幕上的手寫筆)在按下按鈕後移動 4 點,便會拖曳分頁:配備觸控板的 iPad 和以滑鼠拖曳的窄瀏覽器視窗,行為與桌面平台一樣。在 macOS、Windows、Linux 和 GPUI 上,每次按下都視為指標操作,包括觸控螢幕手提電腦上的手指。
  • 在 iOS、iPadOS 和 Android 上,手指長按後才會拖曳分頁,與 iPadOS 的拖放操作一樣,因此在分頁列上滑動會捲動分頁列。
  • 桌面平台和網頁:把手會顯示其軸向的調整大小游標。
  • 手機和窄區域:採用精簡呈現方式,配有抽屜和底部彈層。
  • 所有平台都會繪製應用程式所設定的樣式:分頁、分頁列、把手、放置指示器、停靠區、抽屜和彈層。