Элементы
Dock
Область dock — это рабочее пространство, которое пользователь выстраивает сам: панели в группах вкладок, группы, расположенные рядом или друг над другом, и доки на левом, правом и нижнем краях, которые открываются, закрываются и меняют размер. Вкладка, перетащенная на другую группу, присоединяется к ней, а перетащенная к краю группы делит её. Каждая панель сохраняет своё содержимое при перемещении. DockArea отвечает за поведение: перетаскивание вкладок и их сброс (они выполняются нативно, кадр за кадром), изменение размера разделений и доков, открытие и закрытие доков, увеличение группы и блокировку. Внешний вид остаётся за приложением: вкладки, панели вкладок, ручки, индикатор сброса и содержимое каждой панели отрисовывает и стилизует приложение, а сама область ничего не рисует.
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())
})
}import { mount, signal, Text } from "nsh";
import { DockArea, type DockAreaLayout } from "nsh/base";
const titles: Record<string, string> = {
files: "Files", readme: "README.md", main: "main.rs", preview: "Preview", terminal: "Terminal",
};
function App() {
const layout = signal<DockAreaLayout>({
center: {
type: "split",
axis: "horizontal",
children: [
{ type: "tabs", panels: ["readme", "main"] },
{ type: "tabs", panels: ["preview"] },
],
sizes: [null, 320],
},
left: { layout: { type: "tabs", panels: ["files"] }, size: 240 },
bottom: { layout: { type: "tabs", panels: ["terminal"] }, size: 180 },
});
return (
<DockArea
class="size-full bg-background"
layout={layout()}
onLayoutChange={(e) => layout.set(e.layout)}
handleClass="bg-border group-active:bg-primary"
tabBarClass="flex-row border-b border-border"
tabClass="px-3 py-1.5 selected:bg-muted"
renderTab={(tab) => <Text class="text-sm text-foreground">{titles[tab.panel]}</Text>}
renderPanel={(panel) => <Text class="p-4 text-foreground">{titles[panel.id]} content</Text>}
/>
);
}
mount(App);DockArea и принимаемые им значения берутся из nsh/base, crate nsh-base на Rust (nsh_base::dock_area(), значения в nsh_base::dock). В Rust его создаёт dock_area(): по одному сеттеру на каждое свойство, по одному обработчику на каждое событие и типизированные методы стилей. DockLayout описывает раскладку по ключам панелей (h_split(), v_split(), tabs(), .child(layout, size), .panel(key)).
#Раскладка
Раскладка — это значение DockAreaLayout: центр и по одному доку на левом, правом и нижнем краях, каждый из которых необязателен. Каждая область — это дерево из узлов двух видов:
| Узел | Содержит | Поля |
|---|---|---|
{ type: "split" } | Другие узлы, расположенные рядом (axis: "horizontal") или друг над другом ("vertical") | children и sizes: длина каждого дочернего узла вдоль оси в пунктах, null — разделить оставшееся место |
{ type: "tabs" } | Панели по ключам, показывается одна за раз | panels (ключи в порядке вкладок), activeIndex (показываемая панель, по умолчанию 0) |
Панель всегда находится в группе вкладок: область с одной панелью — это группа вкладок из одной панели. Ключ панели — это строка, которую приложение выбирает для неё ("files", id документа), и каждый ключ встречается во всей раскладке только один раз. Док (DockRegion) — это его дерево (layout), его размер поперёк края (size, по умолчанию 200, не менее 100), признак того, открыт ли он (open, по умолчанию true), и признак того, закрывается ли он, если перетащить его край меньше минимального размера (collapsible, по умолчанию true).
У каждого узла есть id. В раскладке, которую пишет приложение, id можно опустить: область назначит их сама, а каждая сообщаемая ею раскладка содержит их. Если передать сообщённую раскладку обратно, каждая группа сохранит свою идентичность, по которой методы ref обращаются к группам. Область также приводит полученное в порядок: пустая группа или разделение исчезает, разделение из одного дочернего узла заменяется этим узлом, разделение внутри разделения с той же осью сливается с родителем, activeIndex остаётся в допустимых пределах, ключ, указанный дважды, сохраняет только первое место. Сообщаемые раскладки всегда имеют такую единую каноническую форму.
Раскладка принадлежит приложению (layout): каждое изменение, которое вносит пользователь (выбор, перетаскивание или закрытие вкладки, изменение размера разделения или дока, открытие или закрытие дока), сообщается как вся новая раскладка (onLayoutChange), а область показывает раскладку, которую приложение передаёт обратно. Само приложение меняет раскладку, передавая новое значение: восстановленную сохранённую раскладку, команду, которая перестраивает рабочее пространство. Область без layout хранит свою раскладку, начиная с defaultLayout, и по-прежнему сообщает о каждом изменении.
#Содержимое панелей
Что показывает панель, решает приложение: renderPanel отрисовывает её по ключу панели (panel.id). Размещаются все панели группы, а показывается только отображаемая, поэтому при переключении вкладок содержимое каждой панели сохраняется. Когда панель перемещается (в другую группу, в док, в увеличенный режим и обратно), вместе с ней перемещается то же самое содержимое: созданные ею сигналы, текст поля ввода, положение прокрутки, страница веб-представления — всё это остаётся. Область отрисовывает панель один раз, при её первом появлении, и освобождает её содержимое, когда панель покидает раскладку (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 задаёт, что разрешено каждой панели, по ключу; панель, которой нет в списке, может всё:
| Поле | По умолчанию | Описание |
|---|---|---|
id | Ключ панели. | |
closable | true | Её можно закрыть. |
zoomable | true | Её группу можно увеличить, пока панель отображается. |
visible | true | Она показывается. Скрытая панель сохраняет своё место и возвращается на него; группа или разделение, все панели которых скрыты, не занимают места. |
#Вкладки
У каждой группы есть панель вкладок с вкладкой для каждой видимой панели, в порядке её panels. renderTab отрисовывает то, что показывает вкладка (заголовок, значок, кнопку закрытия приложения); без него вкладка показывает ключ панели. Область сама делает это вкладкой: нажатие выбирает её, перетаскивание перемещает её панель, программы чтения с экрана воспринимают её как вкладку с состоянием выбора, а классы selected: у tabClass стилизуют отображаемую вкладку. Значение вкладки содержит всё, что приложению нужно для её отрисовки:
| Поле | Тип | Описание |
|---|---|---|
panel | string | Ключ панели. |
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, формат файла раскладки, который использует и dock в gpui-base.
#Телефоны и узкие окна
Dock подстраивается под собственную ширину. Область шириной 600 пунктов и больше (в окне на десктопе, в вебе, на планшете, на iPad в полноэкранном режиме, на раскрытом складном устройстве) — это полноценное рабочее пространство: разделения, группы и доки расположены рядом, как в многопанельных приложениях для планшетов. При управлении пальцем на телефоне или планшете перетаскивание вкладки начинается с долгого нажатия, как в drag and drop на iPadOS, поэтому панель вкладок по-прежнему прокручивается смахиванием, а ручки получают более широкую область касания при том же внешнем виде.
Область уже 600 пунктов (на телефоне, на iPad в Slide Over, в узком окне десктопа или браузера либо в узкой колонке широкого окна) представляет ту же раскладку для одного экрана, подобно тому как split view на iPad сворачивает свои колонки, а supporting pane в 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} />}
/>
#Props
| Prop | Rust | Type | Default | Description |
|---|---|---|---|---|
layout | .layout(..) | DockAreaLayout | нет | Отображаемая раскладка (controlled): передайте обратно то, что сообщает 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> | нет | Ползунок каждого разделителя: рукоятка по центру линии, нарисованная вертикально, как на вертикальной линии, и повёрнутая на четверть оборота на горизонтальной, так что один класс подходит для обеих осей. Он остаётся тем же представлением, пока разделитель находится под курсором, нажат и перетаскивается, поэтому классы group-hover: и group-active: задают его переходы (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> | нет | Компактное представление: нижняя панель (sheet) нижнего дока. |
backdropClass, backdropStyle | .backdrop_style(|s| ..) | string, StyleProp<ViewStyle> | нет | Компактное представление: подложка за открытой выдвижной или нижней панелью; нажатие на неё закрывает панель. |
handleAccessibilityLabel | .handle_accessibility_label(..) | string | нет | Что говорят программы чтения с экрана для каждого разделителя, между группами разделения и у края каждого дока. |
ref | .component_ref(&r) (component_ref::<DockArea>()) | DockAreaRef | Методы области. | |
class, style | style methods | string, StyleProp<ViewStyle> | нет | Сама область: её размер в родителе (flex-1), её фон. Она остаётся в своих границах и не берёт минимальный размер от содержимого. |
#Events
| Event | Rust | Description |
|---|---|---|
onLayoutChange | .on_layout_change(|layout: &DockAreaLayout| ..) | Раскладка изменилась (перетаскивание, выбор, изменение размера, открытие или закрытие дока, вызов метода): { layout }, вся новая раскладка со всеми идентификаторами. |
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 }: док был показан или скрыт: изменился его layout open в полном представлении, его выдвижная или нижняя панель была показана или закрыта в компактном, либо представление переключилось. |
#Parts
| Part | Inside | Styled with | States |
|---|---|---|---|
| Область | 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 (и их стили) |
#Accessibility
- Каждая вкладка для программ чтения с экрана является вкладкой с состоянием «выбрана», как и
Tab, а её активация выбирает её. Её подпись — это текст, который рисуетrenderTab. - Каждый разделитель, между группами разделения и у края каждого дока, является регулируемым элементом с подписью
handleAccessibilityLabel. Разделители между группами разделения принадлежат Resizable: программа чтения с экрана сдвигает их по 10 пунктов за раз, сообщая об этом черезonLayoutChange. - В компактном представлении Escape и «назад» закрывают показанную выдвижную или нижнюю панель.
Общие сведения о подписях, ролях и объявлениях см. в разделе Accessibility.
#Platform notes
- Указатель (мышь, трекпад, перо, зависшее над экраном) начинает перетаскивание вкладки, как только сдвигается на 4 пункта с нажатой кнопкой, на любой платформе: iPad с трекпадом и узкое окно браузера, управляемое перетаскиванием мышью, ведут себя так же, как настольный компьютер. В macOS, Windows, Linux и GPUI каждое нажатие считается нажатием указателя, включая палец на сенсорном экране ноутбука.
- Палец в iOS, iPadOS и Android перетаскивает вкладку после долгого нажатия, как при drag and drop в iPadOS, поэтому смахивание по панели вкладок прокручивает её.
- Настольные платформы и веб: разделители показывают курсор изменения размера своей оси.
- Телефоны и узкие области: компактное представление с выдвижными панелями и нижней панелью (bottom sheet).
- Каждая платформа отрисовывает то, что стилизует приложение: вкладки, панели вкладок, разделители, индикатор сброса, доки, выдвижные панели и нижнюю панель.