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

Элементы

View

View — основной строительный блок любого экрана: нативный контейнер (UIView на iOS и Mac Catalyst, ViewGroup на Android), который раскладывает дочерние элементы с помощью flexbox или grid, рисует фон, границы, тени и градиенты и обрабатывает нажатия. Кнопки, карточки, строки и экраны — всё это views. В Rust view создаётся с помощью view(), в TSX — с помощью <View> из nsh.

el-view.tsx
el-view.tsxLive
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, Inspector, Panel, Stage, ui, wide } from "./palette";

function App() {
  const presses = signal(0);
  const size = signal("");
  const views = () => (
    <>
      <View
        onPress={() => presses.update((n) => n + 1)}
        {...button("accent", { align: "center" })}
      >
        <Text style={buttonText("accent")}>activeStyle opacity · pressed {presses()}×</Text>
      </View>

      <View
        onPress={() => presses.update((n) => n + 1)}
        activeStyle={{ backgroundColor: ui().border }}
        style={{ ...ui().card, backgroundImage: "none", borderRadius: 16, padding: 16 }}
        hoverStyle={{ opacity: 0.88 }}
        focusStyle={focusRing()}
      >
        <Text style={{ color: ui().text, fontSize: wide() ? 14 : 16 }}>activeStyle background ({desktop ? "click" : "tap"} and hold)</Text>
      </View>

      <View class="flex-row gap-3">
        <View class="flex-1 h-20 rounded-xl bg-[#ff6170] hover:bg-[#ff7d89] active:bg-[#d94d5b] items-center justify-center" focusStyle={focusRing()} onPress={() => {}}>
          <Text class="text-[#13160c] font-semibold">active:</Text>
        </View>
        <View class="flex-1 h-20 rounded-xl bg-[#45d49a] items-center justify-center shadow-lg">
          <Text class="text-[#0c0d0f] font-semibold">shadow-lg</Text>
        </View>
        <View
          class="flex-1 h-20 rounded-xl items-center justify-center"
          style={{ backgroundImage: `linear-gradient(135deg, ${ui().violet}, ${ui().info})` }}
        >
          <Text class="font-semibold" style={{ color: ui().page }}>gradient</Text>
        </View>
      </View>
    </>
  );
  const measured = (height?: number) => (
    <View
      onLayout={({ bounds }) => size.set(`${Math.round(bounds.size.width)} × ${Math.round(bounds.size.height)} pt`)}
      style={{
        flex: height === undefined ? 1 : undefined,
        height,
        borderRadius: 12,
        borderWidth: 2,
        borderStyle: "dashed",
        borderColor: ui().faint,
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Text style={{ color: ui().muted }}>onLayout: {size()}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>
            <Text style={{ color: ui().muted }}>Click and hold the views; resize the window to see onLayout</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={480} gap={16}>
                {views()}
                {measured(120)}
              </Stage>
              <Inspector>
                <Panel title="onPress">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>pressed {presses()}×</Text>
                </Panel>
                <Panel title="onLayout" grow>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{size()}</Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 640, alignSelf: "center", padding: 20, paddingTop: 24, gap: 16 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>
            {views()}
            {measured()}
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    let liked = signal(false);
    view()
        .flex_1()
        .p_6()
        .gap_4()
        .bg(rgb(0xf6f8fa))
        .child(
            view()
                .flex_row()
                .items_center()
                .gap_3()
                .p_4()
                .rounded(px(16.0))
                .bg(white())
                .shadow_md()
                .child(view().size(px(40.0)).rounded_full().bg(rgb(0x8250df)))
                .child(text("A card").flex_1().text_base())
                .child(
                    view()
                        .hit_slop(EdgeInsets::all(px(8.0)))
                        .active(|s| s.opacity(0.5))
                        .on_press(move || liked.update(|l| *l = !*l))
                        .child(text(move || if liked.get() { "♥" } else { "♡" }).text_size(px(22.0))),
                ),
        )
}

#Layout

View раскладывает дочерние элементы с помощью flexbox со следующими значениями по умолчанию: flexDirection: "column", flexShrink: 0, alignContent: "flex-start", position: "relative". display: "grid" (.grid() в Rust) переключает на CSS grid, а display: "block" — на блочную раскладку. Размеры задаются в пунктах. См. раскладку и справочник по стилям.

display: "flex" следует CSS: в стиле, как класс flex и как .flex() в Rust, это строка, если не задано направление (flexDirection: "column", flex-col, .flex_col()). View, в котором display не задан, располагает дочерние элементы в столбец.

#Props

У View есть одно собственное свойство — selectable (.selectable(true) в Rust): тексты внутри view выделяются вместе, как единое выделение (Выделение текста). Остальные свойства — общие свойства: стили (style, class, classList; методы Styled в Rust), disabled, свойства нажатия (onPress, onLongPress, onPressDown, onPressUp, delayLongPress, hitSlop), стили состояний (activeStyle, hoverStyle, focusStyle, disabledStyle, checkedStyle), onMouseEnter / onMouseMove / onMouseLeave, onContextMenu, onFileDragEnter / onFileDragLeave / onFileDrop, focusable, onLayout, ref, nativeID, testID, свойства жестов (pan, pinch, rotation и связанные с ними события), свойства касаний и responder, свойства доступности и ios / android / desktop.

#Selectable views

При selectable выделение проходит по элементам Text и TextView внутри view в порядке чтения; тексты с selectable={false} в выделение не попадают, а selectable view внутри другого входит в состав внешнего. Жесты для каждой платформы см. в разделе Выделение текста.

<View selectable class="gap-3 p-4">
  <Text>One selection runs from here…</Text>
  <Text>…to here.</Text>
</View>

#Press feedback

Нажимаемый view — это любой view с обработчиком нажатия. Отклик задаёт приложение: стиль на время нажатия применяется нативно, пока палец касается экрана, без обращения к коду приложения. hoverStyle, focusStyle и disabledStyle работают так же для остальных состояний:

// Fade the whole view.
view().on_press(open).active(|s| s.opacity(0.6))

// Show a background; group_active styles the children.
view()
    .group()
    .on_press(open)
    .active(|s| s.bg(rgb(0xe5e7eb)))
    .child(text("Open").group_active(|s| s.opacity(0.7)))

// Change any style while pressed.
view().on_press(open).bg(rgb(0x2563eb)).active(|s| s.bg(rgb(0x1e40af)).scale(0.95))

На Android android={{ ripple: { color: "#0000001f" } }} (.ripple(Ripple { .. }) в Rust) добавляет material ripple (borderless, radius и foreground его настраивают).

disabled удаляет слушатели нажатий из нативного view (нет ни нажатия, ни отклика) и применяет стили отключённого состояния:

view()
    .on_press(submit)
    .disabled(move || !valid.get())
    .bg(rgb(0x16a34a))
    .disabled_style(|s| s.opacity(0.4))

#Events

СобытиеRustДанныеКогда
onPress.on_pressPressEvent (в Rust нет)Касание завершилось внутри view.
onLongPress.on_long_pressPressEvent (в Rust нет)Палец оставался прижатым в течение delayLongPress (500 мс).
onPressDown / onPressUp.on_press_down / .on_press_upPressEvent (в Rust нет)Палец опустился / поднялся или ушёл за пределы view.
onMouseEnter / onMouseLeave.on_mouse_enter / .on_mouse_leaveMouseEventМышь вошла в область view (или view внутри него) / покинула её или окно (hover); у onMouseLeave это последняя позиция мыши.
onMouseMove.on_mouse_moveMouseEventМышь переместилась над view (или view внутри него) при нажатой кнопке или без неё, либо клавиша-модификатор была нажата или отпущена, пока мышь находится над view без нажатых кнопок: position, pagePosition (Point), modifiers и pressedButton ("left", "right", "middle", "back", "forward" или null); не чаще одного раза за кадр.
onContextMenu.on_context_menuContextMenuEventВторичная кнопка была нажата над view (контекстные меню).
onFileDragEnter / onFileDragLeave.on_file_drag_enter / .on_file_drag_leaveнетПеретаскивание локальных файлов вошло в активную область назначения на десктопе / покинуло её.
onFileDrop.on_file_drop{ uris: string[] } (FileDropEvent)Локальные файлы или каталоги были сброшены на область назначения на десктопе.
onLayout.on_layout{ bounds } (Bounds<Points>)Изменился frame относительно родителя.
onPanStart / onPanEnd.on_pan_start / .on_pan_endPanEventПеретаскивание, привязанное через pan, началось / закончилось; cancelled равно true, если система отменила его.
onPinchStart / onPinchEnd.on_pinch_start / .on_pinch_end{ scale, velocity }Жест pinch, привязанный через pinch, начался / закончился.
onRotationStart / onRotationEnd.on_rotation_start / .on_rotation_end{ rotation, velocity }Жест поворота, привязанный через rotation, начался / закончился.
onAccessibilityAction.on_accessibility_action{ actionName }Выполнено пользовательское действие доступности.

События касаний и responder: см. общие свойства.

#Desktop file drops

На AppKit, GTK, Win32 и GPUI view с onFileDrop принимает локальные файлы и каталоги, перетащенные из другого приложения. onFileDragEnter и onFileDragLeave отслеживают активную область назначения; onFileDrop получает { uris: string[] } в том порядке, в каком их отдаёт нативный список файлов. Каждый элемент — абсолютный URL file: с экранированными пробелами и не-ASCII-символами. Чтобы получить путь в файловой системе, разберите URL, а не удаляйте префикс вручную.

let hovering = signal(false);
let files = signal(Vec::<String>::new());

view()
    .min_h(px(160.0))
    .p_6()
    .on_file_drag_enter(move || hovering.set(true))
    .on_file_drag_leave(move || hovering.set(false))
    .on_file_drop(move |event: &FileDropEvent| {
        files.set(event.uris.clone());
        hovering.set(false);
    })
    .child(text(move || if hovering.get() { "Release files here" } else { "Drop files here" }))
    .child(text(move || files.get().join("\n")))

Drop получает ближайший включённый предок, который слушает onFileDrop. Поиск ограничивается корнем модального окна. disabled удаляет слушатели перетаскивания файлов; выход, отмена, завершение и удаление области назначения сбрасывают активное состояние. Приём использует операцию Copy и смонтированное нативное состояние. Читать, открывать или передавать полученные файлы — решает приложение.

#Ref methods

Ref простого view — это ViewRef: focus(), blur() и measure(). ref (.element_ref(&handle) в Rust) возвращает его узел, например чтобы передать в setAccessibilityFocus (API доступности nsh) или чтобы измерить его (handle.measure() в Rust).

#Views inside text

View, помещённый внутрь текста, — это встроенное вложение: он располагается на базовой линии, переносится вместе со словами и самостоятельно обрабатывает нажатия. См. text.

#Platform notes

  • boxShadow отрисовывается на всех платформах; android={{ elevation }} вместо этого включает material-тень Android. На Android для тени нужен цвет фона (view с прозрачным фоном тень не отбрасывает).
  • filter для views: iOS поддерживает opacity(), brightness() до 1 и drop-shadow() (изображения принимают любые цветовые фильтры и blur()); Android поддерживает все начиная с API 31, а ниже — только цветовые фильтры.
  • backdropFilter: "blur(…)" — системный материал на iOS (приближение) и требует API 31 на Android.
  • Android отрисовывает трансформации translate, scale и rotate; наклоны (skew(), skewX(), skewY()) применяются на остальных платформах.
  • borderCurve: "continuous" (.border_curve(BorderCurve::Continuous)) рисует углы, плавно переходящие в стороны (как у иконок приложений iOS), на обеих платформах.
  • Стиль cursor применяется там, где есть указатель (Mac Catalyst, iPad с указателем, десктопы).