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

Элементы

Resizable

Группа с изменяемыми размерами раскладывает панели рядом или друг над другом, с ручкой на каждой границе между двумя из них: боковая панель рядом с документом, редактор над консолью, список рядом с подробностями. Пользователь перетаскивает ручку, чтобы сдвинуть границу, и каждая панель остаётся в пределах размеров, которые разрешает приложение. ResizablePanelGroup отвечает за поведение: перетаскивание, которое выполняется нативно покадрово, распределение места, курсор изменения размера и увеличенную область касания для пальца. Внешний вид определяет приложение: ручка представляет собой полосу без собственной линии, цвета или элемента захвата, а то, что она рисует, является индикатором приложения; панели являются представлениями приложения.

el-resizable.tsx
el-resizable.tsxLive
import { mount, signal, Text, View } from "nsh";
import { ResizablePanel, ResizablePanelGroup } from "nsh/base";
import { desktop, Log, Panel, Segmented, Split, ui, wide } from "./palette";

// The sizes are the app's: the group reports them when a drag ends, and the
// app passes them back as each panel's `size`.
function App() {
  const sidebar = signal(desktop ? 220 : 140);
  const consoleSize = signal(120);
  const showConsole = signal<"Shown" | "Hidden">("Shown");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  const round = (sizes: number[]) => sizes.map((s) => Math.round(s)).join(" · ");

  // The handles' look: a hairline that lights up under the pointer and while dragged.
  const handle = "bg-border group-hover:bg-muted-foreground group-active:bg-primary";

  const label = (title: string, detail: string) => (
    <View style={{ gap: 4 }}>
      <Text style={{ fontSize: 13, fontWeight: "600", color: ui().text }}>{title}</Text>
      <Text style={{ fontSize: 12, color: ui().muted }}>{detail}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>resizable</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Drag a line between two panels" : "Drag a line between two panels with your finger"}
      </Text>

      <Split
        grow
        width={640}
        inspector={
          <>
            <Panel title="sizes">
              <Text style={{ fontSize: 13, fontVariant: ["tabular-nums"], color: ui().text }}>
                {`sidebar ${Math.round(sidebar())} · console ${Math.round(consoleSize())}`}
              </Text>
            </Panel>
            <Segmented label="console" options={["Shown", "Hidden"] as const} value={showConsole()} onChange={(v) => showConsole.set(v)} />
            <Log title="onResize" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 360 : 300, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <ResizablePanelGroup
            axis="horizontal"
            handleClass={handle}
            handleAccessibilityLabel="Resize the sidebar"
            onResize={({ sizes }) => log(`row: ${round(sizes)}`)}
          >
            <ResizablePanel size={sidebar()} minSize={100} maxSize={320} onResize={({ size }) => sidebar.set(size)}>
              <View style={{ flex: 1, padding: 14, gap: 10, backgroundColor: ui().inset.backgroundColor }}>
                {label("Files", `${Math.round(sidebar())} pt, 100 to 320`)}
              </View>
            </ResizablePanel>
            <ResizablePanel minSize={120}>
              <ResizablePanelGroup
                axis="vertical"
                handleClass={handle}
                handleAccessibilityLabel="Resize the console"
                onResize={({ sizes }) => log(`column: ${round(sizes)}`)}
              >
                <ResizablePanel minSize={80}>
                  <View style={{ flex: 1, padding: 14 }}>{label("Editor", "Takes the room left")}</View>
                </ResizablePanel>
                <ResizablePanel
                  size={consoleSize()}
                  minSize={60}
                  fixed
                  visible={showConsole() === "Shown"}
                  onResize={({ size }) => consoleSize.set(size)}
                >
                  <View style={{ flex: 1, padding: 14 }}>{label("Console", "Fixed: keeps its height")}</View>
                </ResizablePanel>
              </ResizablePanelGroup>
            </ResizablePanel>
          </ResizablePanelGroup>
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{h_resizable, resizable_panel, theme};
use nsh_core::prelude::*;

fn workspace() -> impl IntoElement {
    h_resizable()
        .handle_style(|s| s.bg(move || theme().border()))
        .handle_accessibility_label("Resize the sidebar")
        .on_resize(|sizes: &Vec<Points>| log::info!("sizes: {sizes:?}"))
        .child(
            resizable_panel()
                .size(px(240.0))
                .size_range(px(160.0)..px(480.0))
                .p_4()
                .child(text("Files").text_color(move || theme().muted_foreground())),
        )
        .child(
            resizable_panel()
                .p_4()
                .child(text("Editor").text_color(move || theme().foreground())),
        )
}

ResizablePanelGroup и ResizablePanel поставляются в nsh/base, crate nsh-base для Rust. Панели являются прямыми дочерними элементами группы. В Rust h_resizable() и v_resizable() создают группу по каждой оси (resizable_panel_group().axis(..) принимает ось как значение), а resizable_panel() создаёт панель, с сеттером для каждого свойства, обработчиком для каждого события, типизированными методами стиля и .child(..) для их содержимого; .size_range(min..max) задаёт оба ограничения сразу.

#Размеры

size панели — это размер, который она хочет получить, в пределах её minSize и maxSize. Панели без него равномерно делят оставшееся место независимо от содержимого; когда размер есть у каждой панели, оставшееся место делится пропорционально им. Пока пользователь перетаскивает ручку, группа перераспределяет место между панелями в ядре, без участия JS:

  • Когда панель перед ручкой растёт, она забирает место у панелей после неё, начиная с ближайшей, каждую вплоть до её minSize, пропуская fixed.
  • Когда она уменьшается ниже своего minSize, она забирает место у панелей перед ней.
  • Ни одна панель не вырастает больше своего maxSize, и сумма панелей никогда не превышает размер группы.

Когда перетаскивание завершается и размеры изменились, группа сообщает размер каждой видимой панели в порядке их расположения (onResize у группы), а каждая панель, чей размер изменился, сообщает свой собственный (onResize у панели). Панели, которые сдвинуло перетаскивание, сохраняют размеры пользователя, пока приложение не передаст новый size; приложение сохраняет их и после жизни группы, передавая сообщённые размеры обратно как size, так что раскладка, восстановленная из хранилища, открывается с теми размерами, которые оставил пользователь. Сообщают только перетаскивания и шаги скринридеров: изменение размера окна, показ или скрытие панели ничего не сообщают.

Когда группа растёт или уменьшается (изменяется размер окна), панели с размером сохраняют его, а панели без размера принимают разницу на себя. Когда группа слишком мала для заданных размеров, панели уменьшаются, за исключением fixed (боковая панель, инспектор), и возвращаются к своим размерам, когда место появляется снова. Панель с visible={false} не занимает места: остальные делят её пространство, и она возвращается со своим размером, когда снова показывается.

<ResizablePanelGroup axis="vertical" handleClass="bg-border">
  <ResizablePanel>
    <Editor />
  </ResizablePanel>
  <ResizablePanel size={180} minSize={120} fixed visible={consoleOpen()}>
    <Console />
  </ResizablePanel>
</ResizablePanelGroup>

#Ручки

На каждой границе между двумя видимыми панелями есть ручка: полоса толщиной 9 пунктов поперёк оси, центрированная на границе, с курсором изменения размера для своей оси. На сенсорных экранах её область касания выходит ещё на 10 пунктов в каждую сторону, поэтому даже линию толщиной в один пункт легко захватить. Ручка является group: то, что она рисует, является индикатором приложения, стилизуемым с помощью handleClass / handleStyle: 1 пункт поперёк и вся длина границы, поэтому один bg-border даёт тонкую линию на любой оси, а w-[3px] или h-[3px] делает её толще, и её классы group-hover: и group-active: следуют за указателем над полосой и за нажатием или перетаскиванием. Для чего-то большего, чем линия (захват из точек, значок), renderHandle отрисовывает содержимое индикатора, получая от ручки index, axis и state; он перерисовывается при смене состояния, несколько раз за перетаскивание, но не при каждом движении. Анимированный захват является бегунком ручки (handleThumbClass / handleThumbStyle): одно представление, центрированное на линии, нарисованное вертикально и повёрнутое на четверть оборота на горизонтальной линии, так что один класс подходит для обеих осей; его показывают и анимируют классы group-hover: и group-active:; ручка остаётся нажатой на всё время перетаскивания, куда бы ни ушёл указатель.

<ResizablePanelGroup
  handleClass="w-[3px] items-center justify-center bg-border"
  renderHandle={(handle) => (
    <View class={handle.state === "dragging" ? "h-8 w-1 rounded-full bg-primary" : "h-6 w-1 rounded-full bg-muted-foreground"} />
  )}
>
  {/* panels */}
</ResizablePanelGroup>

disabled оставляет каждую ручку на месте (заблокированная раскладка).

#Свойства

#ResizablePanelGroup

СвойствоRustТипПо умолчаниюОписание
axish_resizable(), v_resizable(), .axis(..)"horizontal" | "vertical""horizontal"Рядом (ручки перетаскиваются влево и вправо) или друг над другом (вверх и вниз).
disabled.disabled(..)booleanfalseБез перетаскивания: ручки остаются на месте.
handleClass, handleStyle.handle_style(|s| ..)string, StyleProp<ViewStyle>нетИндикатор каждой ручки, на границе внутри полосы ручки: 1 пункт поперёк оси и вся длина границы, если не указано иное (w-[3px], h-[3px]); group-hover: и group-active: следуют за полосой.
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).
renderHandle.render_handle(|handle| ..)(handle: ResizeHandleContext) => JSX.ElementнетСодержимое каждого индикатора, перерисовывается при изменении state ручки.
handleAccessibilityLabel.handle_accessibility_label(..)stringнетЧто скринридеры произносят для каждой ручки: что она изменяет. Они меняют её по 10 пунктов за раз.
children.child(..)JSX.ElementнетResizablePanel, её прямые дочерние элементы.
class, styleметоды стиляstring, StyleProp<ViewStyle>size-fullСама группа; она заполняет родителя, если её класс не говорит иначе.

#ResizablePanel

СвойствоRustТипПо умолчаниюОписание
size.size(..)numberнетЖелаемая длина вдоль оси группы, в пунктах, в пределах minSize и maxSize; новое значение изменяет размер. Без него панель делит оставшееся место.
minSize.min_size(..), .size_range(..)number100Наименьший размер панели.
maxSize.max_size(..), .size_range(..)numberбез ограниченийНаибольший размер панели.
fixed.fixed(..)booleanfalseСохраняет свой размер, когда группа слишком мала для панелей; перетаскивание её собственной ручки всё равно изменяет его. Принимает size.
visible.visible(..)booleantrueСкрытая панель не занимает места, а остальные делят её пространство.
children.child(..)JSX.ElementнетЕё содержимое.
class, styleметоды стиляstring, StyleProp<ViewStyle>нетСобственная поверхность панели. Её размер вдоль оси задаёт группа.

#ResizeHandleContext

ПолеТипОписание
indexnumberГраница после index-й видимой панели.
axis"horizontal" | "vertical"Ось, вдоль которой она изменяет размер.
state"idle" | "hovered" | "pressed" | "dragging"Насколько далеко ушли вместе с ней указатель или палец.

#События

СобытиеRustОписание
onResize (ResizablePanelGroup).on_resize(|sizes: &Vec<Points>| ..)Перетаскивание завершилось и сдвинуло ручку, либо скринридер её изменил: sizes, размер каждой видимой панели в порядке их расположения (слева направо, сверху вниз).
onResize (ResizablePanel).on_resize(|size: &Points| ..)Перетаскивание завершилось и изменило размер этой панели: size, новый размер, который нужно передать обратно как её size.

#Части

ЧастьВнутриСтилизуется черезСостояния
ГруппаResizablePanelGroup: class, style
ПанельгруппаResizablePanel: class, style
Индикатор ручкиполоса ручки, на границеhandleClass, handleStyle, renderHandlegroup-hover: под указателем, group-active: при нажатии или перетаскивании; state в renderHandle
Бегунок ручкипо центру линии ручкиhandleThumbClass, handleThumbStylegroup-hover:, group-active: (нажат до конца перетаскивания)

#Доступность

  • Каждая ручка является для скринридеров регулируемым элементом с меткой handleAccessibilityLabel. Её значение — размер предшествующей ей панели в пределах minSize и maxSize этой панели.
  • Смахивание вверх или вниз по ручке в VoiceOver или клавиши громкости в TalkBack сдвигают её на 10 пунктов, как и перетаскивание; группа сообщает об этом через onResize. Ручки группы disabled остаются на месте.
  • Содержимое панелей принадлежит приложению и читается так же, как и везде.

Подробнее о метках, ролях и объявлениях в целом см. в разделе Доступность.

#Примечания по платформам

  • Настольные платформы (macOS, Windows, Linux, GPUI) и веб: указатель над ручкой показывает курсор изменения размера столбца или строки, а перетаскивание мышью или тачпадом сдвигает границу.
  • iOS, Android и Mac Catalyst: палец перетаскивает ручку в пределах области касания, которая шире полосы на 10 пунктов с каждой стороны.
  • В раскладках справа налево панели и их размеры следуют порядку раскладки.
  • Каждая платформа рисует то, что стилизует приложение: индикаторы и панели.
  • Разделения в Dock являются изменяемыми группами: их размер изменяется так же.