العناصر
المنطقة الآمنة
تُبقي المنطقة الآمنة المحتوى بعيدًا عن النتوء (notch) أو Dynamic Island، وعن شريط الحالة، ومؤشر الشاشة الرئيسية، ورأس التنقل، وشريط عنوان Mac Catalyst. يمكن لأي عرض أن يضيف حشوة بمقدارها باستخدام env() في CSS:
env(safe-area-inset-top) وenv(safe-area-inset-right) وenv(safe-area-inset-bottom) و
env(safe-area-inset-left)، ضمن الحشوة (padding) أو الهامش (margin). وهي تمثل الهوامش الداخلية للعرض الجذر الذي تُرسم فيه العقدة: داخل NavPage تكون الهوامش الواقعة أسفل رأس الصفحة، وداخل Dialog تكون هوامش مربع الحوار، وفي غير ذلك تكون المنطقة الآمنة للسطح. في المتصفح تكون الصفحة هي الجذر، وتكون المتغيرات هي متغيرات المتصفح نفسه.
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))
}import { mount, Text, View } from "nsh";
function App() {
return (
<View
style={{
flex: 1,
backgroundColor: "#fff8c5",
paddingTop: "env(safe-area-inset-top)",
paddingRight: "env(safe-area-inset-right)",
paddingBottom: "env(safe-area-inset-bottom)",
paddingLeft: "env(safe-area-inset-left)",
}}
>
<Text style={{ fontSize: 24, fontWeight: "700" }}>Clear of the notch</Text>
</View>
);
}
mount(App);يملأ لون الخلفية المنطقة كلها، بما في ذلك المنطقة خلف شريط الحالة، ويبدأ المحتوى أسفلها. وتتبع القيم الهوامش: عند التدوير، أو في صفحة أو مربع حوار آخر، تتحدث الحشوة تلقائيًا بصورة أصلية.
#القيم
| القيمة | مثال | المعنى |
|---|---|---|
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()<View class="flex-1 pt-[env(safe-area-inset-top)] px-[calc(env(safe-area-inset-left)+16px)]">
…
</View>#متى تستخدمها
تضع NavPage ذات الرأس المرئي والمعتم محتواها أصلًا أسفل الرأس، لذا يكون الهامش العلوي هناك 0. وتُستخدم هوامش المنطقة الآمنة في:
- جذر تطبيق بلا مكدس تنقل،
- صفحة تستخدم
headerVisible={false}أوheaderTransparent، - محتوى Dialog بملء الشاشة،
- أشرطة خاصة بك عند حواف الشاشة.
#safeArea وsafeAreaInsets
عندما تحتاج إلى الأرقام في الشيفرة، اقرأها مباشرة. تأتي دالتا الوصول في TSX من nsh، وتُرجعان كائن Edges
({ top, right, bottom, left } بالنقاط)، وهما تفاعليتان: الشيفرة التي تقرؤهما تُعاد عند تغيّرهما، مثلًا عند التدوير.
| TSX | Rust | القيمة المُرجعة |
|---|---|---|
safeArea() | safe_area() | المنطقة الآمنة للسطح: الأجزاء الخالية من واجهة النظام (النتوء، مؤشر الشاشة الرئيسية، شريط العنوان). |
safeAreaInsets() | cx.insets() | الهوامش في الموضع الذي يُرسم فيه المكوّن: هوامش أقرب صفحة (أسفل رأسها)، أو أقرب مربع حوار، وإلا فالمنطقة الآمنة للسطح. وهذه هي قيم متغيرات safe-area-inset-*. في TSX استدعها داخل مكوّن؛ فهي تُرجع دالة وصول (accessor). ويقرؤها مكوّن Rust في الحزمة (crate) الخاصة بك باستخدام 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"))
}import { Text, View, safeArea } from "nsh";
function BottomBar() {
const insets = safeArea();
return (
<View style={{ flexDirection: "row", padding: 8, paddingBottom: insets().bottom > 0 ? 4 : 8 }}>
<Text>Bottom bar</Text>
</View>
);
}لإضافة حشوة بمقدار الهامش نفسه، يُعد env() الصيغة الأبسط: فهو يتحدث بصورة أصلية دون تشغيل شيفرة.
#ملاحظات المنصات
- iOS: تأتي الهوامش من المنطقة الآمنة في UIKit: شريط الحالة، والنتوء أو Dynamic Island، ومؤشر الشاشة الرئيسية.
- Android: يُبلغ NSH عن أجزاء أشرطة النظام وقصّة الشاشة (display cutout) التي تتداخل مع عرض NSH. وعندما يكون NSH مضمَّنًا أسفل شريط أدوات أصلي، يكون الهامش العلوي للعرض 0.
- Mac Catalyst: الهامش العلوي هو شريط عنوان النافذة.
- أنظمة سطح المكتب (macOS وWindows وLinux وGPUI) والويب: يبدأ محتوى الصفحة أسفل
header، ويمنح مربع الحوار محتواه هوامش خاصة به، كما في الهواتف.
#ذو صلة
- لوحة المفاتيح:
env(keyboard-inset-height)، إبقاء المحتوى فوق لوحة المفاتيح البرمجية. - الجهاز والبيئة: المنطقة الآمنة بين قيم الجهاز الأخرى.