العناصر
Overlay
يعرض Overlay عناصره الفرعية فوق كل شيء في العرض الجذري (root view) الموجود فيه: النافذة، أو مربع الحوار المحيط به.
لا يقصّه حجم العنصر الأب ولا overflow: hidden ولا عروض التمرير. ويبقى في موضعه الذي كتبته فيه بالنسبة لكل شيء
آخر: فهو يقرأ السياق المحيط به، ومعالجاته (handlers) تابعة لمكوّنه، ويزول بزوال عنصره الأب. تُبنى عليه النوافذ المنبثقة
والقوائم وتلميحات الأدوات والصفائح (Popovers وMenus وTooltips وSheets)؛ ويوفّرها nsh/base جاهزة
(Popover وMenu وTooltip وSheet)، أما Overlay فهو ما
تستخدمه لبناء مكوّناتك الخاصة.
import { mount, Overlay, signal, Text, View, type ViewRef } from "nsh";
import { button, buttonText, desktop, Inspector, Panel, Segmented, Stage, ui, wide } from "./palette";
type Placement = "top" | "bottom" | "left" | "right";
type Align = "start" | "center" | "end";
function App() {
const open = signal(false);
const placement = signal<Placement>("bottom");
const align = signal<Align>("center");
const position = signal("—");
const closed = signal("—");
let anchor: ViewRef | undefined;
const placements = () => (
<Segmented
label="placement"
options={["top", "bottom", "left", "right"] as Placement[]}
value={placement()}
onChange={(v) => (placement.set(v), open.set(true))}
/>
);
const aligns = () => (
<Segmented label="align" options={["start", "center", "end"] as Align[]} value={align()} onChange={(v) => (align.set(v), open.set(true))} />
);
const subject = () => (
<>
<View
style={{
width: 220,
height: 72,
overflow: "hidden",
borderRadius: 14,
borderWidth: 2,
borderStyle: "dashed",
borderColor: ui().faint,
alignItems: "center",
justifyContent: "center",
}}
>
<View
ref={anchor}
onPress={() => open.set(!open())}
{...button("accent", { align: "center" })}
>
<Text style={buttonText("accent")}>{open() ? "Close" : "Open"}</Text>
</View>
<If when={open()}>
<Overlay
anchor={anchor}
placement={placement()}
align={align()}
offset={8}
margin={12}
onPosition={(e) =>
position.set(
`${e.placement} at ${Math.round(e.bounds.origin.x)}, ${Math.round(e.bounds.origin.y)} · ${Math.round(e.bounds.size.width)}×${Math.round(e.bounds.size.height)} · room ${Math.round(e.available.width)}×${Math.round(e.available.height)}`,
)
}
onPressOutside={(e) => {
closed.set(`onPressOutside at ${Math.round(e.pagePosition.x)}, ${Math.round(e.pagePosition.y)}`);
open.set(false);
}}
onCancel={() => {
closed.set("onCancel");
open.set(false);
}}
style={{
...ui().card,
borderRadius: 12,
padding: 14,
gap: 4,
// Narrow beside the button, so it fits on a phone's side.
maxWidth: !wide() && (placement() === "left" || placement() === "right") ? 130 : 220,
}}
>
<Text style={{ fontSize: 15, fontWeight: "600", color: ui().text }}>On top of everything</Text>
<Text style={{ fontSize: 13, color: ui().muted }}>
Its box clips the button, not this overlay. Pick a side {wide() ? "on the right" : "above"}.
</Text>
</Overlay>
</If>
</View>
<Text style={{ marginTop: 6, fontSize: 12, color: ui().faint }}>The dashed box has overflow: hidden</Text>
</>
);
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>overlay</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Click the button, then click outside or press Escape" : "Tap the button, then tap outside"}
</Text>
{() =>
wide() ? (
<View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
<Stage>
<View style={{ alignItems: "center" }}>{subject()}</View>
</Stage>
<Inspector>
{placements()}
{aligns()}
<Panel title="onPosition">
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{position()}</Text>
</Panel>
<Panel title="closed by" grow>
<Text style={{ fontSize: 13, color: closed() === "—" ? ui().text : ui().accent }}>{closed()}</Text>
</Panel>
</Inspector>
</View>
) : (
<>
<View style={{ flexDirection: "row", gap: 12 }}>
<View style={{ flex: 3 }}>{placements()}</View>
<View style={{ flex: 2 }}>{aligns()}</View>
</View>
<View style={{ flex: 1, minHeight: 160, alignItems: "center", justifyContent: "center" }}>{subject()}</View>
<View style={{ ...ui().inset, borderRadius: 12, padding: 12, gap: 6 }}>
<Text style={{ fontSize: 12, color: ui().muted }}>
onPosition <Text style={{ color: ui().text }}>{position()}</Text>
</Text>
<Text style={{ fontSize: 12, color: ui().muted }}>
closed by <Text style={{ color: closed() === "—" ? ui().text : ui().accent }}>{closed()}</Text>
</Text>
</View>
</>
)
}
</View>
);
}
mount(App);use nsh_core::prelude::*;
fn more_button() -> impl IntoElement {
let open = signal(false);
let anchor = element_ref();
view()
.child(
view()
.element_ref(&anchor)
.on_press(move || open.update(|o| *o = !*o))
.px_3()
.py_2()
.child(text("More")),
)
.child(when(move || open.get(), move || {
overlay()
.anchor(&anchor)
.placement(Placement::Bottom)
.align(Align::Start)
.offset(px(6.0))
.margin(px(8.0))
.on_press_outside(move |_: &Point<Points>| open.set(false))
.on_cancel(move || open.set(false))
.rounded_lg()
.bg(white())
.p_3()
.shadow_lg()
.child(text("Shown above everything"))
}))
}import { Overlay, signal, Text, View, type ViewRef } from "nsh";
function MoreButton() {
const open = signal(false);
let anchor: ViewRef | undefined;
return (
<View>
<View ref={anchor} onPress={() => open.set(!open())} class="px-3 py-2">
<Text>More</Text>
</View>
<If when={open()}>
<Overlay
anchor={anchor}
placement="bottom"
align="start"
offset={6}
margin={8}
onPressOutside={() => open.set(false)}
onCancel={() => open.set(false)}
class="rounded-lg bg-white p-3 shadow-lg"
>
<Text>Shown above everything</Text>
</Overlay>
</If>
</View>
);
}يأتي Overlay من nsh في TSX، وoverlay() من nsh_core::prelude في Rust. يظهر ما دام موجودًا في
العرض: ضعه داخل If (في Rust: when(..)) لفتحه وإغلاقه. تظهر عناصر Overlay التي تُعرض لاحقًا فوق التي قبلها، وأي
Overlay داخل آخر يظهر فوقه.
#التموضع
بدون anchor، يُوضع Overlay كصندوق position: fixed في العرض الجذري: فقيم top وleft وinset
والحجم والهوامش تُحسب بالنسبة إلى النافذة بأكملها (أو مربع الحوار). class="inset-0" يغطيها، وهذا يناسب الخلفية الداكنة أو
الطبقة التي تملأ النافذة كلها؛ أما class="bottom-0 left-0 right-0" فهو شريط على امتداد الأسفل.
مع anchor، يضعه NSH بجوار نقطة الارتكاز (anchor):
anchorهو عرض (ref)، أوPoint({ x, y }) بإحداثيات العرض الجذري، أوpageX/pageYللمسة (قائمة تظهر حيث يكون المؤشر)، أوBounds({ origin, size }) فيه: مثل مؤشر الإدخال في حقل نصي (pageBoundsالخاص بـonCaretMove)، حيث تُفتح اقتراحات لما تتم كتابته.placementهو الجانب الذي يوضع عليه:"top"أو"bottom"(الافتراضي) أو"left"أو"right". وإذا لم يتسع له ذلك الجانب واتسع له الجانب المقابل، ذهب إليه؛ وإذا لم يتسع له أي منهما، وُضع على الجانب الأكثر اتساعًا.alignيحاذيه على امتداد ذلك الجانب:"start"أو"center"(الافتراضي) أو"end". فوق نقطة الارتكاز وتحتها، يتبع start وend اتجاه التخطيط: start هو الحافة اليسرى، أو الحافة اليمنى في التخطيط من اليمين إلى اليسار.offsetهو الفجوة بين نقطة الارتكاز وOverlay؛ وmarginهي المسافة التي يحافظ عليها من حواف المنطقة الآمنة (safe area) التي يبقى داخلها: فينزلق على امتداد جانبه ليتسع له المكان.
يتبع Overlay المرتكز نقطةَ ارتكازه: فعندما تتمرر نقطة الارتكاز أو تتحرك أو يتغير حجمها، يتحرك Overlay معها في
الإطار نفسه الذي يضع فيه NSH التخطيط. ويخبرك onPosition أين استقر، فيمكن لسهم أن يشير إلى نقطة الارتكاز، أو لقائمة طويلة أن
تحدّ من ارتفاعها:
<Overlay
anchor={anchor}
onPosition={(e) => {
side.set(e.placement); // "top" when it flipped
maxHeight.set(e.available.height);
}}
/>
#الإغلاق
يبلّغ Overlay بما يغلق النوافذ المنبثقة عادةً، ويترك القرار لك:
onPressOutsideيُطلق عندما ينزل إصبع أو زر الفأرة خارج Overlay ونقطة ارتكازه وعناصر Overlay الموجودة داخله. ويستمر الضغط إلى ما تحته.onCancelيُطلق عند الضغط على Escape، أو زر الرجوع في Android، أو إيماءة الهروب في VoiceOver على iOS، وذلك لأعلى Overlay يستمع إليه فقط. ويستقبلها قبل الصفحات وBackHandler.
لحجب الشاشة خلف نافذة منبثقة، ضع Overlay ثانيًا تحتها مع class="inset-0" وonPress يغلق
كليهما: هكذا يعمل Sheet وعرض Popover على الهاتف.
#الخصائص
| الخاصية | Rust | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|---|
anchor | .anchor(&element_ref), .anchor_point(point), .anchor_bounds(bounds) | ViewRef | Point | Bounds | null | لا شيء | العرض أو النقطة أو الصندوق الذي يوضع بالنسبة إليه. بدونه يكون صندوقًا ثابتًا في عرضه الجذري. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "bottom" | الجانب من نقطة الارتكاز الذي يوضع عليه. |
align | .align(..) | "start" | "center" | "end" | "center" | كيف يحاذي نقطة الارتكاز على امتداد ذلك الجانب. |
offset | .offset(px(..)) | number | 0 | الفجوة بين نقطة الارتكاز وOverlay. |
margin | .margin(px(..)) | number | 0 | المسافة التي يحافظ عليها من حواف المنطقة الآمنة. |
children | .child(..) | JSX.Element | لا شيء | ما يعرضه. |
class, style | style methods | string, StyleProp<ViewStyle> | لا شيء | مظهره، ومكانه عند عدم وجود نقطة ارتكاز. |
كما يقبل الخصائص المشتركة للعروض.
#الأحداث
| الحدث | Rust | الوصف |
|---|---|---|
onPressOutside | .on_press_outside(|point| ..) | حدث ضغط خارج Overlay ونقطة ارتكازه وعناصر Overlay الموجودة داخله (pagePosition، وPoint). |
onCancel | .on_cancel(|| ..) | Escape أو زر الرجوع في Android أو إيماءة الهروب في VoiceOver، عندما يكون أعلى Overlay مستمعًا. |
onPosition | .on_position(|position| ..) | وضعه NSH: placement (الجانب الذي استقر عليه)، وbounds في العرض الجذري، وavailable (Size: المساحة المتاحة على ذلك الجانب). |
#ملاحظات المنصات
- تعرض كل منصة Overlay كعرض أصلي (native view) ضمن العرض الجذري للنافذة (أو مربع الحوار)، يُرسم ويُختبر لمس الضغط عليه كما تُرسم العروض المحيطة به: UIKit وعروض Android وAppKit وGTK وWin32 وGPUI وDOM.
- يظهر Overlay داخل Dialog في ذلك الحوار، فوق محتواه.
- تبقى عناصر Overlay داخل نافذتها: فالقائمة التي كانت ستتجاوز حافة النافذة تنزلق عائدةً إلى داخلها.