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

Элементы

Безопасная область

Безопасная область (safe area) не даёт контенту оказаться под вырезом или Dynamic Island, строкой состояния, индикатором «Домой», навигационным заголовком и строкой заголовка Mac Catalyst. Любое представление может учитывать её отступы с помощью CSS-функции env(): env(safe-area-inset-top), env(safe-area-inset-right), env(safe-area-inset-bottom) и env(safe-area-inset-left) в своих padding или margin. Это отступы корневого представления, в котором отображается узел: внутри NavPage это отступы под заголовком страницы, внутри Dialog это отступы диалога, а в остальных случаях это безопасная область поверхности. В браузере корнем является страница, а переменные принадлежат самому браузеру.

el-safe-area.tsx
el-safe-area.tsxLive
import { mount, signal, Text, View } from "nsh";
import { safeArea, safeAreaInsets } from "nsh";
import { button, buttonText, desktop, tint, ui } from "./palette";

type Edge = "top" | "right" | "bottom" | "left";

const EDGES: Edge[] = ["top", "right", "bottom", "left"];

// `calc(env(safe-area-inset-<edge>) + 16px)` on the edges chosen, 16 px on the others.
const side = (edges: Edge[], e: Edge) => (edges.includes(e) ? (`calc(env(safe-area-inset-${e}) + 16px)` as const) : 16);
const padding = (edges: Edge[]) => ({
  paddingTop: side(edges, "top"),
  paddingRight: side(edges, "right"),
  paddingBottom: side(edges, "bottom"),
  paddingLeft: side(edges, "left"),
});

const format = (i: { top: number; right: number; bottom: number; left: number }) =>
  `top ${Math.round(i.top)} · right ${Math.round(i.right)} · bottom ${Math.round(i.bottom)} · left ${Math.round(i.left)}`;

// Called in a component: the insets where it renders (the root here, so the
// surface's safe area; inside a page or a dialog, theirs).
function Insets() {
  const insets = safeAreaInsets();
  return <Text style={{ color: ui().text, fontVariant: ["tabular-nums"] }}>{format(insets())}</Text>;
}

function App() {
  const edges = signal<Edge[]>([...EDGES]);
  const toggle = (edge: Edge) => edges.update((list) => (list.includes(edge) ? list.filter((e) => e !== edge) : EDGES.filter((e) => e === edge || list.includes(e))));
  const label = () => ({ fontSize: 12, color: ui().muted });
  return (
    // The tinted band around the card is the view's padding: the insets of
    // the edges chosen, plus 16 pt.
    <View style={{ flex: 1, backgroundColor: tint(ui().accent, 0.16), ...padding(edges()) }}>
      <View
        style={{
          flex: 1,
          width: "100%",
          maxWidth: 560,
          alignSelf: "center",
          backgroundColor: ui().page,
          borderRadius: 16,
          borderWidth: 1,
          borderStyle: "dashed",
          borderColor: ui().faint,
          padding: 20,
          gap: 14,
        }}
      >
        <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>safe area insets</Text>
        <Text style={{ color: ui().muted, lineHeight: 20 }}>
          {desktop
            ? "A desktop window or a browser page has no system UI over it: the insets are 0, so the view pads only by its 16 pt (the tinted band)."
            : "On a phone the insets are the status bar or notch on top and the home indicator below; env(safe-area-inset-*) adds them to its 16 pt. This browser frame has none, so they are 0 here."}
        </Text>

        <Text style={label()}>edges ({desktop ? "click" : "tap"} to toggle)</Text>
        <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
          <Each of={EDGES}>
            {(edge) => {
              const on = () => edges().includes(edge());
              return (
                <View
                  role="button"
                  onPress={() => toggle(edge())}
                  {...button("secondary", { on })}
                >
                  <Text style={buttonText("secondary", { on })}>{edge()}</Text>
                </View>
              );
            }}
          </Each>
        </View>

        <View style={{ ...ui().inset, borderRadius: 12, padding: 14, gap: 6 }}>
          <Text style={label()}>edges</Text>
          <Text style={{ color: ui().text }}>{edges().length ? `[${edges().map((e) => `"${e}"`).join(", ")}]` : "[] (no inset padded)"}</Text>
          <Text style={label()}>safeArea() · the surface's insets</Text>
          <Text style={{ color: ui().text, fontVariant: ["tabular-nums"] }}>{format(safeArea())}</Text>
          <Text style={label()}>safeAreaInsets() · where it renders</Text>
          <Insets />
        </View>
      </View>
    </View>
  );
}

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

fn app() -> impl IntoElement {
    view()
        .flex_1()
        .bg(rgb(0xfff8c5))
        .p_safe_area()
        .child(text("Clear of the notch").text_2xl().font_weight(FontWeight::BOLD))
}

Цвет фона заполняет всю область, включая область за строкой состояния, а контент начинается под ней. Значения следуют за отступами: при повороте экрана, на другой странице или в другом диалоге padding обновляется нативно.

#Значения

ЗначениеПримерЧто означает
env(name)paddingTop: "env(safe-area-inset-top)"Отступ.
env(name, <length>)env(safe-area-inset-top, 20px)Отступ; длина подставляется вместо неизвестного имени, как в CSS.
calc(env(name) + <length>)calc(env(safe-area-inset-bottom) + 16px)Отступ плюс длина, для контента, который сохраняет собственные интервалы.
calc(<length> + env(name))calc(16px + env(safe-area-inset-left))То же самое.

Они применяются к padding, paddingHorizontal, paddingVertical, paddingTop, paddingRight, paddingBottom, paddingLeft и к тем же ключам margin. В Tailwind это произвольные значения утилит p, px, py, pt, pr, pb, pl и m (пробелы можно опустить): pt-[env(safe-area-inset-top)], pb-[calc(env(safe-area-inset-bottom)+16px)]. Тип этих значений в TypeScript: EnvValue. См. справочник по стилям.

В Rust их задают методы Styled: .p_safe_area(), .px_safe_area(), .py_safe_area(), .pt_safe_area(), .pr_safe_area(), .pb_safe_area(), .pl_safe_area(), те же методы m и .pt_safe_area_plus(px(16.0)) (а также pr, pb, pl, mt, mr, mb, ml) для отступа плюс длины.

#Выбор сторон

Добавляйте отступ только с тех сторон, где он нужен, например когда нижний отступ обрабатывает панель вкладок или ваша собственная нижняя панель:

view()
    .flex_1()
    .pt_safe_area()
    .px_safe_area()

#Где использовать

NavPage с видимым непрозрачным заголовком уже размещает свой контент под заголовком, поэтому верхний отступ там равен 0. Отступы безопасной области нужны для:

  • корня приложения без навигационного стека,
  • страницы с headerVisible={false} или headerTransparent,
  • контента полноэкранного Dialog,
  • ваших собственных панелей у краёв экрана.

#safeArea и safeAreaInsets

Когда числа нужны в коде, читайте их напрямую. Оба TSX-аксессора поступают из nsh, возвращают объект Edges ({ top, right, bottom, left } в пунктах) и реактивны: код, который их читает, выполняется снова при их изменении, например при повороте экрана.

TSXRustВозвращает
safeArea()safe_area()Безопасная область поверхности: части, свободные от системного UI (вырез, индикатор «Домой», строка заголовка).
safeAreaInsets()cx.insets()Отступы там, где отображается компонент: отступы ближайшей страницы (под её заголовком), ближайшего диалога или, иначе, безопасная область поверхности. Это значения переменных safe-area-inset-*. В TSX вызывайте её внутри компонента; она возвращает аксессор. Rust-компонент вашего крейта читает их с помощью cx.insets().
use nsh_core::prelude::*;

fn bottom_bar() -> impl IntoElement {
    view()
        .flex_row()
        .p_2()
        .pb(move || if safe_area().bottom > Points::ZERO { px(4.0) } else { px(8.0) })
        .child(text("Bottom bar"))
}

Для padding, равного самому отступу, проще использовать env(): он обновляется нативно, без выполнения кода.

#Особенности платформ

  • iOS: отступы берутся из безопасной области UIKit: строка состояния, вырез или Dynamic Island и индикатор «Домой».
  • Android: NSH сообщает те части системных панелей и выреза дисплея, которые перекрывают представление NSH. Когда NSH встроен под нативную панель инструментов, верхний отступ представления равен 0.
  • Mac Catalyst: верхний отступ соответствует строке заголовка окна.
  • Настольные платформы (macOS, Windows, Linux, GPUI) и веб: контент страницы начинается под её header, а диалог задаёт для своего контента собственные отступы, как и на телефонах.

#См. также