Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Dock

Область 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, 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Ключ панели.
closabletrueЕё можно закрыть.
zoomabletrueЕё группу можно увеличить, пока панель отображается.
visibletrueОна показывается. Скрытая панель сохраняет своё место и возвращается на него; группа или разделение, все панели которых скрыты, не занимают места.

#Вкладки

У каждой группы есть панель вкладок с вкладкой для каждой видимой панели, в порядке её panels. renderTab отрисовывает то, что показывает вкладка (заголовок, значок, кнопку закрытия приложения); без него вкладка показывает ключ панели. Область сама делает это вкладкой: нажатие выбирает её, перетаскивание перемещает её панель, программы чтения с экрана воспринимают её как вкладку с состоянием выбора, а классы selected: у tabClass стилизуют отображаемую вкладку. Значение вкладки содержит всё, что приложению нужно для её отрисовки:

ПолеТипОписание
panelstringКлюч панели.
groupnumberId группы вкладок, в которой она находится.
indexnumberЕё место среди видимых вкладок группы.
activebooleanЭто отображаемая вкладка группы.
closablebooleanОбласть закрыла бы её: она закрываема, область не заблокирована, и её группа может её отдать.
zoomedbooleanЕё группа увеличена.
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

PropRustTypeDefaultDescription
layout.layout(..)DockAreaLayoutнетОтображаемая раскладка (controlled): передайте обратно то, что сообщает onLayoutChange.
defaultLayout.default_layout(..)DockAreaLayoutпустой центрНачальная раскладка, когда layout не задан; в дальнейшем область хранит собственную.
locked.locked(..)booleanfalseВкладки нельзя перетаскивать, сбрасывать или закрывать.
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, stylestyle methodsstring, StyleProp<ViewStyle>нетСама область: её размер в родителе (flex-1), её фон. Она остаётся в своих границах и не берёт минимальный размер от содержимого.

#Events

EventRustDescription
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

PartInsideStyled withStates
Областьclass, style
Группа вкладокрегионgroupClass, groupStyle
Панель вкладокгруппаtabBarClass, tabBarStyle
Вкладкапанель вкладокtabClass, tabStyle; renderTabselected:, active:, hover:
Содержимое группыгруппаcontentClass, contentStyle; renderPanel
Индикатор разделителямежду группами разделения, у края докаhandleClass, handleStylegroup-hover:, group-active:
Ползунок разделителяпо центру линии разделителяhandleThumbClass, handleThumbStylegroup-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).
  • Каждая платформа отрисовывает то, что стилизует приложение: вкладки, панели вкладок, разделители, индикатор сброса, доки, выдвижные панели и нижнюю панель.