تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

API

nsh

nsh هي الوحدة الأساسية: واجهة البرمجة التفاعلية (الإشارات signals، والقيم المحسوبة computed، والتأثيرات effects، والسياق context)، وmount، وtoken، وعناصر المضيف View وText وScroll وOverlay، والعروض القابلة للتحديد، والمنطقة الآمنة ولوحة المفاتيح، والرسوم المتحركة، والجهاز وبيئته، وPanResponder، والحافظة، ومنتقي الصور، والتعرف على الكلام، والتخزين، وقناة المضيف، واختصارات لوحة المفاتيح، والمكونات والوحدات الأصلية، وإمكانية الوصول، وأنواع TypeScript الخاصة بالعناصر والأنماط والأحداث. يعمل الرسم التفاعلي نفسه أصلياً بلغة Rust؛ وقراءة الإشارة هي استدعاء إلى بيئة التشغيل يكلّف بضعة نانوثوانٍ، ولا يعمل في مفسّر JavaScript سوى الدوال المغلقة (closures) التي تكتبها أنت.

import { batch, cleanup, computed, context, effect, ErrorBoundary, mount, Overlay, Scroll, signal, split, Text, token, untracked, View } from "nsh";
import { Input } from "nsh/base";

يتبع التفاعل في NSH النموذج نفسه المستخدم في SolidJS: تعمل دالة المكوّن مرة واحدة، وتُحدَّث أجزاء الواجهة التي تقرأ الإشارات في مكانها عندما تتغير تلك الإشارات. لا يُعاد رسم المكوّن ولا يوجد DOM افتراضي. راجع التفاعلية للاطلاع على الدليل.

#signal

function signal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
function signal<T = undefined>(): Signal<T | undefined>;

interface Signal<T> {
  (): T;                              // read (tracked)
  set(value: T): void;                // write
  update(fn: (value: T) => T): void;  // write from the current value
}

interface SignalOptions<T> {
  /** When a new value counts as a change (default `===`); `false`: always. */
  equals?: false | ((previous: T, next: T) => boolean);
}

تحمل الإشارة قيمة. استدعاؤها يقرأ القيمة، وداخل نطاق التتبع (تعبيرات JSX، وcomputed، وeffect) يشترك ذلك النطاق فيها. يكتب set وupdate القيمة، ويتحدّث كل ما قرأها.

import { mount, signal, Text, View } from "nsh";

function Counter() {
  const count = signal(0);
  return (
    <View style={{ padding: 24, gap: 12 }}>
      <Text>Count: {count()}</Text>
      <Text onPress={() => count.update((n) => n + 1)}>Add one</Text>
    </View>
  );
}

mount(Counter);

افتراضياً، لا يفعل أي شيء الإسناد الذي === للقيمة الحالية. مرّر equals: false للإشعار عند كل كتابة (مفيد عندما تعدّل كائناً في مكانه ثم تسند الكائن نفسه مرة أخرى)، أو مرّر دالة المقارنة الخاصة بك:

const items = signal<string[]>([], { equals: false });
const point = signal({ x: 0, y: 0 }, { equals: (a, b) => a.x === b.x && a.y === b.y });

#computed

function computed<T>(fn: (previous: T | undefined) => T, options?: SignalOptions<T>): Accessor<T>;

interface Accessor<T> {
  (): T;
}

قيمة مشتقة من إشارات. يعمل fn مرة أخرى عندما تتغير إشارة قرأها؛ ولا تتحدّث الجهات القارئة للقيمة المحسوبة إلا عندما تتغير نتيجتها (بحسب equals، وهي === افتراضياً). يستقبل fn النتيجة السابقة.

const first = signal("Ada");
const last = signal("Lovelace");
const full = computed(() => `${first()} ${last()}`);
full(); // "Ada Lovelace"

استخدم computed عندما يكون الاشتقاق مكلفاً أو يُقرأ في عدة أماكن. أما التعبير الرخيص فدالة عادية (const full = () => first() + " " + last()) تفاعلية بالقدر نفسه.

#effect

function effect(fn: () => void): void;

يشغّل fn بعد العرض، ومرة أخرى كلما تغيّرت إشارة قرأها. استخدم التأثيرات للآثار الجانبية: التسجيل (logging)، والمؤقتات، والتخاطب مع الوحدات الأصلية. ينتمي التأثير إلى المكوّن (أو التأثير) الذي أُنشئ فيه ويُتخلص منه معه.

const query = signal("");
effect(() => {
  console.log("query is now", query());
});

للتراجع عن شيء قبل أن يعمل التأثير مرة أخرى (أو عندما يزول مكوّنه)، استدعِ cleanup داخله.

#batch

function batch<T>(fn: () => T): T;

يشغّل fn ويطبّق كل عمليات الكتابة فيه معاً في النهاية، فتتحدّث الجهات القارئة مرة واحدة بدلاً من مرة لكل كتابة. يعيد ما يعيده fn.

batch(() => {
  first.set("Grace");
  last.set("Hopper");
}); // `full` recomputes once

#untracked

function untracked<T>(fn: () => T): T;

يشغّل fn دون اشتراك النطاق الحالي فيما يقرؤه. استخدمه لقراءة إشارة داخل تأثير أو تعبير JSX دون الاعتماد عليها.

effect(() => {
  // Runs again when `query` changes, not when `page` changes.
  search(query(), untracked(() => page()));
});

#cleanup

function cleanup(fn: () => void): void;

يسجّل fn ليعمل قبل أن يعمل التأثير الحالي مرة أخرى، أو عندما يزول المكوّن أو التأثير الحالي.

function Clock() {
  const now = signal(new Date());
  const timer = setInterval(() => now.set(new Date()), 1000);
  cleanup(() => clearInterval(timer));
  return <Text>{now().toLocaleTimeString()}</Text>;
}

#context

function context<T>(defaultValue: T): Context<T>;

interface Context<T> extends Accessor<T> {
  Provider: (props: { value: T; children?: JSX.Element }) => JSX.Element;
}

قيمة يوفّرها مكوّن لكل ما يعرضه، مهما كان عمقه. استدعاء السياق (Theme()) يقرأ قيمة أقرب مزوّد، أو defaultValue عند عدم وجود مزوّد.

import { context, mount, signal, Text, View } from "nsh";

const Theme = context<"light" | "dark">("light");

function Label(props: { text: string }) {
  return <Text style={{ color: Theme() === "dark" ? "#fff" : "#000" }}>{props.text}</Text>;
}

function App() {
  const dark = signal(false);
  return (
    <Theme.Provider value={dark() ? "dark" : "light"}>
      <View style={{ flex: 1, padding: 24, backgroundColor: dark() ? "#000" : "#fff" }}>
        <Label text="Hello" />
        <Text onPress={() => dark.update((d) => !d)}>Toggle</Text>
      </View>
    </Theme.Provider>
  );
}

mount(App);

#mount

function mount<P extends object>(app: Component<P>, props?: P): () => void;

يعرض app في السطح (النافذة، أو NshAppView / NshView المضمَّن فيه التطبيق) ويعيد دالة تزيله. ينتهي ملف الدخول لكل تطبيق بـ mount(App).

mount(App);

#split

function split<T extends object, K extends readonly (keyof T)[]>(
  props: T,
  keys: K,
): [Pick<T, K[number]>, Omit<T, K[number]>];

يقسّم كائن الخصائص إلى الخصائص المسمّاة في keys وبقية الخصائص. يبقى النصفان تفاعليين: الخصائص التي تتغير قيمها هي getters، ويحتفظ split بها كـ getters. استخدمه في مكوّن يعالج بعض الخصائص بنفسه ويمرّر الباقي. أما تفكيك الخصائص بالتفكيك (const { label } = props) فيقرؤها مرة واحدة ويفقد التحديثات.

import { split, Text, View } from "nsh";
import { Input, type InputProps } from "nsh/base";

function LabeledInput(props: InputProps & { label: string }) {
  const [own, rest] = split(props, ["label"]);
  return (
    <View style={{ gap: 4 }}>
      <Text>{own.label}</Text>
      <Input {...rest} />
    </View>
  );
}

#ErrorBoundary

function ErrorBoundary(props: {
  fallback: JSX.Element | ((error: unknown, reset: () => void) => JSX.Element);
  children?: JSX.Element;
}): JSX.Element;

يعرض أبناءه، أو fallback عندما يُطلق شيء تحتهم استثناءً: جسم مكوّن، أو ربط، أو تأثير، أو معالج حدث. يُزال الجزء الذي فشل ويظهر البديل مكانه. تُستدعى الدالة fallback على الصورة fallback(error, reset) مع القيمة المُطلَقة وreset الذي يعرض الأبناء مرة أخرى، مهما كانت المعاملات التي تعلنها؛ أما العنصر fallback فيُعرض كما هو. ينتقل الفشل إلى أقرب حدّ (boundary) فوقه، والفشل في البديل ينتقل إلى الحد الذي فوقه. تستخدم مكونات Rust catch_error (حدود الأخطاء).

import { ErrorBoundary, Text, View } from "nsh";

function Feed() {
  return (
    <ErrorBoundary
      fallback={(error, reset) => (
        <View>
          <Text>{error instanceof Error ? error.message : String(error)}</Text>
          <Text onPress={reset}>Try again</Text>
        </View>
      )}
    >
      <Posts />
    </ErrorBoundary>
  );
}

#token

function token<K extends keyof Tokens>(name: K): Tokens[K];

رمز التصميم name في سمة التطبيق ("theme" في nsh.json، فوق رموز shadcn/ui) بحسب المظهر المعروض: اللون على صورة #rrggbbaa، والطول كعدد من النقاط، والخط باسم عائلته. الأسماء هي متغيرات CSS الخاصة بالسمة دون -- (primary، muted-foreground، radius، font-mono، والرموز الخاصة بالتطبيق، مثل up). قراءته تفاعلية: يعمل المكوّن أو memo أو التأثير الذي يقرأ رمزاً مرة أخرى عندما تتغير السمة أو المظهر. يسرد Tokens رموز التطبيق مع أنواعها؛ ويكتبه NSH في .nsh/types/nsh/tokens.d.ts انطلاقاً من السمة. بالنسبة إلى الأصناف (classes)، سمِّ الرموز مباشرة (bg-primary، rounded-(--radius)): رموز السمة.

import { effect, token } from "nsh";

effect(() => console.log("primary is", token("primary"), "radius", token("radius")));

#View, Text, Scroll, Overlay

function View(props: ViewProps): JSX.Element;
function Text(props: TextProps): JSX.Element;
function Scroll(props: ScrollProps): JSX.Element;
function Overlay(props: OverlayProps): JSX.Element;

عناصر المضيف التي يصدّرها nsh، تُكتب كوسوم: <View>، <Text>، <Scroll>، <Overlay> (يضع أبناءه أعلى النافذة، موضوعين مقابل نقطة ارتكاز: Overlay). ينشئ المصرّف (compiler) العرض الأصلي في مكانه للوسم الذي يسمّي أحدها عبر استيراده (ويشمل ذلك الاسم المستعار)، لذا فاستيرادها لا يكلّف شيئاً وقت التشغيل. وهي مكوّنات مثل أي مكوّن آخر، فيمكن تمريرها أيضاً (const Tag = inline ? Text : View). يصدّر nsh بقية العناصر الأولية بالطريقة نفسها: List، وImage، وCanvas، وWebView وVideo، ولكل منها صفحة مرجعية خاصة. وخصائصها موثقة ضمن العناصر. أما حقل إدخال النص Input فيأتي من nsh/base.

#المنطقة الآمنة ولوحة المفاتيح

تُبقي متغيرات البيئة في CSS المحتوى بعيداً عن واجهة النظام، في الحشو (padding) والهامش (margin) لأي عرض (padding، paddingTop، …، مفاتيح margin نفسها؛ والقيم الاعتباطية p* / m* في Tailwind؛ والدوال Styled .p_safe_area()، .pt_safe_area_plus(..)، .pb_keyboard()، … في Rust). قيمها هي EnvValue: env(name)، env(name, <length>)، calc(env(name) + <length>) وcalc(<length> + env(name)).

المتغيرالقيمة
safe-area-inset-top، -right، -bottom، -leftهوامش العرض الجذري الذي تُعرض فيه العقدة: هوامش أقرب صفحة (أسفل ترويستها)، أو أقرب حوار، وإلا فالمنطقة الآمنة للسطح. راجع المنطقة الآمنة.
keyboard-inset-heightارتفاع لوحة المفاتيح البرمجية فوق السطح، ويساوي 0 أثناء إخفائها؛ وتتحرك التغييرات بحركة أصلية مع لوحة المفاتيح. راجع لوحة المفاتيح.
<View style={{ flex: 1, paddingTop: "env(safe-area-inset-top)", paddingBottom: "env(keyboard-inset-height)" }}>…</View>

#safeArea and safeAreaInsets

safeArea() هي المنطقة الآمنة للسطح (safe_area() في Rust)؛ وsafeAreaInsets() هي الهوامش حيث يُعرض المكوّن (cx.insets() في Rust)، وهي قيم المتغيرات safe-area-inset-*. كلاهما Edges تفاعلي ({ top, right, bottom, left } بالنقاط). راجع safeArea and safeAreaInsets.

#Keyboard and keyboardFrame

Keyboard.dismiss() يخفي لوحة المفاتيح البرمجية (dismiss_keyboard() في Rust) وkeyboardFrame() يخبرك بمقدار ما تغطيه من الشاشة (keyboard_height() وkeyboard_duration() في Rust). راجع إخفاء لوحة المفاتيح و ارتفاع لوحة المفاتيح.

#العروض القابلة للتحديد

<View selectable> (view().selectable(true) في Rust) يجعل النصوص داخله تُحدَّد معاً، كما في صفحة الويب: تمتد تحديدة واحدة عبر العروض وعناصر Text وعناصر TextView بترتيب القراءة. راجع تحديد النص.

#الرسوم المتحركة

تحرّك واجهة برمجة الرسوم المتحركة على خيط واجهة المستخدم. تنشئ قيمة متحركة، وتربطها (أو القيم المشتقة منها) بخاصية نمط، وتبدأ الحركة؛ ومن ذلك الحين يُحسب كل إطار أصلياً، دون تشغيل شيفرة التطبيق. ولا يمكن لمحرك مشغول أو خيط JS مشغول أن يجعل الحركة متقطعة، ولا تكلّف الحركة التطبيق أي عمل لكل إطار. تستخدم شيفرة Rust animated_value() وmotion() وAnimation وSpring وTiming وKeyframeTiming وMotionPreset وEasing من nsh_core::prelude وتربط القيم بدوال .bind_* الخاصة بالعناصر؛ أما TSX فيستوردها من nsh.

use nsh_core::{element::LayoutAnimation, prelude::*}; // animated_value, motion, Animation, Spring, Timing, Keyframe, KeyframeTiming, MotionPreset, MotionSpec, Easing, animate_layout

راجع الرسوم المتحركة للاطلاع على الدليل.

#ما الذي يمكن أن تقوده القيمة المتحركة

أينRustTSX
opacity.bind_opacity(&value)style={{ opacity: value }}
backgroundColor.bind_background(&value.interpolate_colors(&[0.0, 1.0], &[white(), black()]))style={{ backgroundColor: value.interpolate({ inputRange: [0, 1], outputRange: ["#fff", "#000"] }) }}
borderColor، color.bind_border_color(&node)، .bind_color(&node)الشيء نفسه: عقدة من الألوان. borderColor يلوّن كل الجوانب؛ وcolor الخاص بـ Text يلوّن النص كله
transform.bind_translate_x(&value)، .bind_translate_y، .bind_scale، .bind_rotate (بالدرجات)translateX، translateY، scale، scaleX، scaleY، rotate، rotateX، rotateY، rotateZ خطوات: transform: [{ translateX: value }]
موضع التمرير.scroll_value(&value) على عرض تمرير أو قائمة<Scroll scrollValue={value}> و<List scrollValue={value}> يكتبان إزاحة التمرير فيها في كل إطار
الإيماءات.pan_values(Some(&x), Some(&y))، .pinch_value(&scale)، .rotation_value(&angle)pan={{ x, y }}، pinch={{ scale }}، rotation={{ angle }} على أي عنصر: تكتب الإيماءة القيم مباشرة (راجع اللمس والإيماءات)
أين تلتصق العروض.sticky().bind_sticky_top(&value) (.bind_sticky_left في عرض تمرير أفقي)، .bind_sticky_header_offset(&value) على قائمةstyle={{ position: "sticky", top: value }}، <List stickyHeaderOffset={value}>: تلتصق العروض والترويسات اللاصقة على هذا البعد من بداية إطار العرض، متبعةً القيمة في كل إطار
Canvasمعامل متحرك في استدعاء أداة رسمأي معامل رقمي أو لوني في استدعاء رسم canvas
Shader.uniform(name, &value)عضو في struct Uniforms الخاص بـ shader: uniforms={{ name: value }} (أرقام، أو عقدة من الألوان لـ vec3f / vec4f)

تأخذ خصائص النمط الأخرى قيماً عادية؛ ولتحريك التخطيط نفسه (الأحجام، والمواضع في التدفق)، استخدم LayoutAnimation.

#createAnimatedValue

function createAnimatedValue(value?: number): AnimatedValue;

ينشئ قيمة متحركة تبدأ من value (الافتراضي 0). في Rust: animated_value(value)، وهو مقبض Copy يحتفظ به المكوّن الذي أنشأه.

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    let x = animated_value(0.0);
    let right = stored(false);
    let toggle = move || {
        let to_right = !right.get();
        right.replace(to_right);
        let _ = x.spring(if to_right { 200.0 } else { 0.0 }, Spring::default());
    };
    view()
        .flex_1()
        .p_6()
        .pt_12()
        .gap_4()
        .child(view().size(px(60.0)).rounded(px(12.0)).bg(rgb(0x6366f1)).bind_translate_x(&x))
        .child(text("Move").on_press(toggle))
}

#AnimatedValue

class AnimatedValue extends AnimatedNode {
  constructor(value: number);
  set(value: number): void;
  stop(): Promise<{ value: number }>;
  getValue(): Promise<number>;
  setOffset(offset: number): void;
  flattenOffset(): void;
  extractOffset(): void;
  timing(to: number, options?: { duration?: number; easing?: Easing; delay?: number }): Promise<{ finished: boolean }>;
  spring(to: number, options?: { stiffness?: number; damping?: number; mass?: number; velocity?: number }): Promise<{ finished: boolean }>;
  decay(options?: { velocity?: number; deceleration?: number; clamp?: [number, number]; bounce?: number }): Promise<{ finished: boolean }>;
  keyframes(keyframes: KeyframeValue[], options?: KeyframeTiming): Promise<{ finished: boolean }>;
}
الدالةRustالوصف
set(value)set(value)يقفز إلى value، موقفاً أي حركة جارية.
stop()stop()، ثم value().awaitيوقف الحركة الجارية عند موضعها؛ ويحلّ بـ { value }، أي القيمة عند ذلك الموضع.
getValue()value().awaitيحلّ بالقيمة الحالية (مع إزاحتها)، مقروءةً على خيط واجهة المستخدم.
setOffset(offset)set_offset(offset)إزاحة تُضاف إلى القيمة: ترى الروابط القيمة + الإزاحة؛ وset والحركات والسحب تحرّك القيمة.
flattenOffset()flatten_offset()يضيف الإزاحة إلى القيمة ويجعلها 0.
extractOffset()extract_offset()ينقل القيمة إلى الإزاحة ويجعل القيمة 0 (فيبدأ السحب التالي من حيث انتهى السابق).
timing(to, options)timing(to, ms, easing)، timing_with(to, Timing { … })يتحرك إلى to على منحنى تسهيل (easing). duration بالميلي ثانية (الافتراضي 300)، easing (الافتراضي "easeInOut")، delay بالميلي ثانية (الافتراضي 0).
spring(to, options)spring(to, Spring { … })يرتد كالنابض إلى to. stiffness (الافتراضي 170)، damping (الافتراضي 26)، mass (الافتراضي 1)، velocity (السرعة الابتدائية، بوحدات في الثانية).
decay(options)decay(velocity, deceleration)، decay_within(velocity, deceleration, (min, max), bounce)ينساب انطلاقاً من velocity (بوحدات في الثانية) ويتباطأ بمقدار deceleration لكل ميلي ثانية (الافتراضي 0.998). استخدمه بعد حركة الدفع السريع (fling)، بالسرعة القادمة من onPanEnd. ومع clamp: [min, max] يتوقف عند حد، أو يرتد عنه محتفظاً بـ bounce (من 0 إلى 1، الافتراضي 0) من سرعته.
keyframes(frames, options)keyframes(frames, KeyframeTiming)يشغّل الإطارات المفتاحية كما تُشغَّل الإطارات المفتاحية في CSS: تسهيل لكل مقطع، وiterations مرة في direction.

تعيد كل دالة حركة وعداً (promise) يحلّ بـ { finished }: true عندما انتهت الحركة، وfalse عندما أُوقفت (بواسطة stop أو set أو حركة أخرى للقيمة نفسها). وفي Rust تعيد AnimationEnd، تنتظرها مهمة (task) للحصول على bool نفسه.

let opacity = animated_value(1.0);
spawn(async move {
    if opacity.timing(0.0, 200.0, Easing::EaseInOut).await {
        visible.set(false);
    }
});

#AnimatedNode

class AnimatedNode {
  interpolate(config: {
    inputRange: number[];
    outputRange: number[] | string[];
    extrapolate?: Extrapolation;
    extrapolateLeft?: Extrapolation;
    extrapolateRight?: Extrapolation;
    easing?: Easing;
  }): AnimatedNode;
  diffClamp(min: number, max: number, options?: { snap?: boolean }): AnimatedNode;
  add(b: AnimatedNode | number): AnimatedNode;
  sub(b: AnimatedNode | number): AnimatedNode;
  mul(b: AnimatedNode | number): AnimatedNode;
  div(b: AnimatedNode | number): AnimatedNode;
  mod(b: AnimatedNode | number): AnimatedNode;
}

type Extrapolation = "extend" | "clamp" | "identity";

عقدة في رسم الحركة الأصلي: AnimatedValue، أو قيمة مشتقة من قيم أخرى. تُقيَّم العقد المشتقة على خيط واجهة المستخدم كلما تغيّرت مدخلاتها.

الدالةRustالوصف
interpolate(config)interpolate(&input, &output)، interpolate_colors(&input, &colors)، interpolate_with(&input, output, Interpolation { … })يحوّل المدخل عبر نطاقات مجزأة. يحتوي outputRange على أرقام أو ألوان ("#ff0000"، "rgba(…)") أو زوايا ("45deg"، "1rad"). خارج inputRange يقصّ القيمة افتراضياً؛ ويمكن أن يكون extrapolate (أو extrapolateLeft / extrapolateRight) "extend" (متابعة الخط)، أو "clamp" أو "identity" (تمرير المدخل كما هو). easing يشكّل كل مقطع. يقبل Rust الأرقام وألوان السمة (التي تتبع المظهر) والزوايا بالدرجات لـ bind_rotate.
diffClamp(min, max, options?)diff_clamp(min, max)، diff_clamp_with(min, max, DiffClamp::snapping())يتبع تغيّرات المدخل لكنه يُبقي النتيجة ضمن min..max: ترويسة تختفي أثناء تمرير المحتوى للأعلى وتعود فور التمرير للأسفل. snap: true يثبّتها عند الحد الأقرب خلال 200 ms عندما يستقر عرض التمرير الخاص بها، وعند max فقط بعد أن يبلغ المدخل max.
add، sub، mul، divadd، sub، mul، divعمليات حسابية مع عقدة أخرى أو ثابت.
mod(b)modulo(b)باقي القسمة، لا يكون سالباً أبداً مع b موجب (يُبقي الزوايا ضمن 0..360).
let scroll_y = animated_value(0.0);
let header = scroll_y.diff_clamp(0.0, 44.0);

view()
    .flex_1()
    .child(scroll().scroll_value(&scroll_y).flex_1() /* rows */)
    .child(view().h(px(44.0)).bind_translate_y(&header.mul(-1.0)))

#Keyframes

interface Keyframe {
  offset?: number;          // 0..1 of an iteration; missing offsets spread evenly
  value: number | string;   // a number, or an angle ("45deg", "0.5turn") in degrees
  easing?: Easing;          // the segment that starts here
}
type KeyframeValue = number | string | Keyframe;

interface KeyframeTiming {
  duration?: number;        // one iteration, ms (300)
  delay?: number;           // before the first iteration, ms (0)
  easing?: Easing;          // segments without their own (CSS "ease")
  iterations?: number | "infinite";   // 1
  direction?: "normal" | "reverse" | "alternate" | "alternate-reverse";
}

يشغّل value.keyframes(frames, options) قيمةً عبر إطارات مفتاحية (keyframes) ويُنجَز بالقيمة { finished } التي تبيّن هل اكتملت؛ Animated.keyframes(value, { keyframes, ...options }) مثل خطوة في animation مركّب. Rust: value.keyframes(frames, KeyframeTiming) وAnimation::keyframes(&value, frames, KeyframeTiming)، مع Keyframe::new(value) (موزّعة بالتساوي) وKeyframe::at(offset, value) و.ease(easing)، وKeyframeTiming::new(ms) مع .delay(ms) و.easing(e) و.iterations(Some(n)) و.infinite() و.direction(PlaybackDirection::…).

تعمل كما تعمل إطارات CSS المفتاحية: يشكّل الـ easing مقطعًا واحدًا (easing الإطار المفتاحي للمقطع الذي يبدأ عنده، وeasing التوقيت لكل المقاطع الأخرى)؛ والإزاحات (offsets) الغائبة من الإطارات تُوزَّع بالتساوي، فيكون الأول عند 0 والأخير عند 1؛ وتبدأ كل دورة (iteration) بقفزة إلى أول إطار مفتاحي، وإن لم يكن هناك إطار عند 0 فإن القيمة تنطلق من موضعها الحالي؛ وخلال delay تحتفظ القيمة بأول إطار مفتاحي (في CSS animation-fill-mode: backwards)؛ وتشغّل الدورات العكسية المقاطع بالاتجاه المعاكس مع عكس منحنياتها؛ وتبقى القيمة حيث تتركها الدورة الأخيرة.

let y = animated_value(0.0);
y.keyframes(
    [Keyframe::new(0.0), Keyframe::at(0.3, -20.0).ease(Easing::EaseIn), Keyframe::new(0.0)],
    KeyframeTiming::new(600.0).iterations(Some(3)),
);

#createMotion

function createMotion(options?: MotionOptions): Motion;

interface MotionOptions extends MotionPlayOptions {
  initial?: { [C in MotionChannel]?: number };   // where channels start (the others at rest)
  enter?: MotionName | MotionSpec;   // start where it starts and play it at once
}

interface MotionPlayOptions extends KeyframeTiming {
  ignoreReduceMotion?: boolean;      // play even when the user asks for less motion
}

interface Motion {
  readonly opacity: AnimatedValue;
  readonly translateX: AnimatedValue;
  readonly translateY: AnimatedValue;
  readonly rotate: AnimatedValue;    // degrees
  readonly rotateX: AnimatedValue;
  readonly rotateY: AnimatedValue;
  readonly scale: AnimatedValue;
  readonly scaleX: AnimatedValue;
  readonly scaleY: AnimatedValue;
  readonly style: { opacity: AnimatedValue; transform: MotionTransform };
  readonly transform: MotionTransform;
  play(motion: MotionName | MotionSpec, options?: MotionPlayOptions): Promise<{ finished: boolean }>;
  animation(motion: MotionName | MotionSpec, options?: MotionPlayOptions): CompositeAnimation;
  stop(): void;
  reset(): void;
}

ينشئ حركة (motion): قيمة متحركة لكل قناة من قنوات opacity وtransform في العرض، في حالة السكون (opacity والمقاييس 1، والبقية 0)، وحركات تُشغَّل عليها بشكل native. اربطه بـ style={motion.style}، أو وزّع motion.style أو motion.transform داخل style؛ يطبّق التحويل الإزاحات أولًا ثم التدويرات ثم المقاييس، مع دمج scale في scaleX وscaleY: 7 من خطوات التحويل الثماني للعرض، مما يترك واحدة لك ([{ perspective: 600 }, ...motion.transform] للقلب ثلاثي الأبعاد). initial يحدد أين تبدأ القنوات ({ opacity: 0 }؛ وتبدأ البقية في حالة السكون). مع enter، تبدأ القنوات التي تحرّكها من حيث تبدأ تلك الحركة (خلال initial) وتُشغَّل فورًا، وتكون الخيارات الأخرى هي توقيتها (delay وduration و…). تحرّك الحركة opacity وtransform فقط: توزيع motion.style يضبط opacity للعرض، لذا اربط motion.transform فقط إن أردت الاحتفاظ بـ opacity خاصة بك، وحرّك الألوان بقيمة متحركة وinterpolate. Rust: motion()، وهو مقبض Copy يحتفظ به المكوّن الذي أنشأه، ويُربط بـ .bind_motion(&m)؛ وmotion().entering(spec) تشغّل حركة الدخول.

العضوRust (Motion)الوصف
opacity، translateX، … scaleYvalue(MotionChannel::Opacity)، …القيم المتحركة للقنوات.
style، transform.bind_motion(&m) على العنصرعمليات الربط.
play(motion, options?)play(spec)، play_ignoring_reduce_motion(spec)يشغّل preset بالاسم أو MotionSpec على القنوات؛ وتقاطع الحركةُ الجديدة الحركةَ الجارية. options تغيّر توقيتها. يُنجَز بالقيمة { finished }: true عند اكتمالها، وfalse عندما أوقفها شيء ما (AnimationEnd في Rust).
animation(motion, options?)animation(spec)الحركة نفسها بصيغة CompositeAnimation غير مبدوءة، لتركيبها: Animated.stagger(60, cards.map((m) => m.animation("fadeInUp")), { from: "center" }) (Rust: Animation::stagger_from(60.0, StaggerFrom::Center, cards.iter().map(|m| m.animation(MotionPreset::FadeInUp)))). يُحدَّد وضع الحركة المخفَّضة عند إنشائها.
stop()stop()يوقفها حيث هي.
reset()reset()يوقفها ويعيد كل قناة إلى حالة السكون.

عندما يطلب المستخدم حركة أقل (reduceMotion())، تصل play وanimation وenter فورًا إلى حيث تنتهي الحركة، دون أن تتحرك، ما لم يُضبط ignoreReduceMotion (في الخيارات أو في الحركة).

let card = motion().entering(MotionPreset::FadeInUp.spec().timing(|t| t.delay(80.0)));
view().size(px(120.0)).rounded(px(16.0)).bg(rgb(0x0969da)).bind_motion(&card).on_press(move || {
    card.play(MotionPreset::Pulse);
})

#motions

const motions: Readonly<Record<MotionName, MotionPreset>>;

type MotionChannel = "opacity" | "translateX" | "translateY" | "rotate" | "rotateX" | "rotateY" | "scale" | "scaleX" | "scaleY";

interface MotionSpec extends KeyframeTiming {
  keyframes: { [C in MotionChannel]?: KeyframeValue[] } | MotionFrame[];
  ignoreReduceMotion?: boolean;
}
type MotionFrame = { [C in MotionChannel]?: number | string } & { offset?: number; easing?: Easing };

interface MotionPreset extends MotionSpec {
  kind: "enter" | "exit" | "attention" | "loop";
}

الحركات الجاهزة كبيانات: motion.play("shake") يشغّل motions.shake. MotionName هو أحد fadeIn أو fadeInUp أو fadeInDown أو fadeInLeft أو fadeInRight أو zoomIn أو popIn أو bounceIn أو slideInUp أو slideInDown أو flipInX أو flipInY (دخول)؛ fadeOut أو fadeOutUp أو fadeOutDown أو fadeOutLeft أو fadeOutRight أو zoomOut أو popOut أو slideOutDown أو slideOutUp (خروج)؛ shake أو headShake أو wiggle أو swing أو pulse أو heartbeat أو rubberBand أو jello أو tada أو jump أو flash (لفت الانتباه)؛ spin أو float أو breathe أو sway أو bob أو glow (تكرار). تصف صفحة Animation كل واحدة منها. وزّع preset لتعديله: motion.play({ ...motions.fadeInUp, duration: 600 }).

MotionSpec هي حركة من تصميمك: الإطارات المفتاحية لكل قناة ({ opacity: [0, 1], translateY: [24, 0] })، أو قائمة إطارات كما تكتبها Web Animations API ([{ opacity: 0, translateY: 24 }, { opacity: 1, translateY: 0 }]، والإطار بلا offset يُوضع بحسب موضعه في القائمة)، مع توقيت الإطارات المفتاحية. Rust: MotionSpec::new(KeyframeTiming) مع .values(channel, values) (موزّعة بالتساوي) و.track(channel, keyframes) و.timing(|t| …)؛ MotionPreset::FadeInUp (أو .spec()) هو preset، وMotionChannel يسمّي القنوات.

let rise = MotionSpec::new(KeyframeTiming::new(400.0))
    .values(MotionChannel::Opacity, [0.0, 1.0])
    .track(MotionChannel::TranslateY, [Keyframe::at(0.0, 24.0).ease(Easing::EaseOut), Keyframe::at(1.0, 0.0)]);
card.play(rise);

#Animated

const Animated: {
  timing(value: AnimatedValue, config: { toValue: number; duration?: number; easing?: Easing; delay?: number }): CompositeAnimation;
  spring(value: AnimatedValue, config: { toValue: number; stiffness?: number; damping?: number; mass?: number; velocity?: number }): CompositeAnimation;
  decay(value: AnimatedValue, config?: { velocity?: number; deceleration?: number; clamp?: [number, number]; bounce?: number }): CompositeAnimation;
  sequence(list: CompositeAnimation[]): CompositeAnimation;
  parallel(list: CompositeAnimation[], config?: { stopTogether?: boolean }): CompositeAnimation;
  stagger(ms: number, list: CompositeAnimation[], config?: { from?: "first" | "last" | "center" | number }): CompositeAnimation;
  delay(ms: number): CompositeAnimation;
  loop(animation: CompositeAnimation, config?: { iterations?: number; resetBeforeIteration?: boolean }): CompositeAnimation;
  keyframes(value: AnimatedValue, config: KeyframeTiming & { keyframes: KeyframeValue[] }): CompositeAnimation;
  add(a: AnimatedNode, b: AnimatedNode | number): AnimatedNode;
  subtract(a: AnimatedNode, b: AnimatedNode | number): AnimatedNode;
  multiply(a: AnimatedNode, b: AnimatedNode | number): AnimatedNode;
  divide(a: AnimatedNode, b: AnimatedNode | number): AnimatedNode;
  modulo(a: AnimatedNode, b: AnimatedNode | number): AnimatedNode;
  diffClamp(a: AnimatedNode, min: number, max: number, options?: { snap?: boolean }): AnimatedNode;
  sampleSpring(config: SpringConfig & { from?: number }, fps?: number): number[];
  sampleDecay(config: DecayConfig & { from?: number }, fps?: number): number[];
};

interface CompositeAnimation {
  start(callback?: (result: { finished: boolean }) => void): Promise<{ finished: boolean }>;
  stop(): void;
  reset(): void;
}

يبني Animated الرسوم المتحركة من خطوات (timing وspring وdecay وsequence وparallel و…). لا يعمل شيء حتى start()؛ عندها تعمل الشجرة كلها بشكل native، وتبدأ كل خطوة على UI thread في اللحظة نفسها التي تنتهي فيها السابقة (بلا فجوة بين الإطارات، وبلا ذهاب وإياب إلى شيفرة التطبيق بين الخطوات). يبني Rust الشجرة نفسها بـ Animation.

الدالةRustالوصف
timing، spring، decayAnimation::timing(&value, to, Timing)، Animation::spring(&value, to, Spring)، Animation::decay(&value, velocity, deceleration)animation واحد لقيمة واحدة؛ بالخيارات والقيم الافتراضية نفسها لدوال AnimatedValue، مع الهدف في toValue.
sequence(list)Animation::sequence(list)يشغّل الرسوم المتحركة واحدًا تلو الآخر.
parallel(list, { stopTogether })Animation::parallel(list).stop_together(…)يشغّلها في الوقت نفسه. مع stopTogether (الافتراضي true)، إيقاف واحد يوقف الجميع.
stagger(ms, list, { from })Animation::stagger(ms, list)، Animation::stagger_from(ms, StaggerFrom::Center, list)يشغّلها بالتوازي، ويبدأ كل واحد بعد ms من سابقه، محسوبًا من from: "first" (الافتراضي) أو "last" أو "center" (يبدأ الأوسط فورًا وتتوزع البقية انطلاقًا منه؛ وإذا كان العدد زوجيًا يبدأ العنصران الأوسطان فورًا) أو فهرس (StaggerFrom::First وLast وCenter وIndex(i)).
keyframes(value, { keyframes, ...timing })Animation::keyframes(&value, frames, KeyframeTiming)قيمة عبر الإطارات المفتاحية.
delay(ms)Animation::delay(ms)ينتظر؛ مفيد داخل sequence.
loop(animation, { iterations, resetBeforeIteration })animation.looped(iterations).reset_before_iteration(…)يكرّر animation iterations مرة (إلى ما لا نهاية افتراضيًا)، ويعيد قيمه قبل كل تشغيل ما لم يكن resetBeforeIteration هو false.
add، subtract، multiply، divide، modulo، diffClampدوال العقدةمثل دوال AnimatedNode، لكن كدوال مستقلة.
sampleSpring، sampleDecaynsh_core::element::sample_spring، sample_decayقيم spring أو decay، قيمة واحدة لكل إطار (انظر Easing).

CompositeAnimation:

الدالةRust (Animation)الوصف
start(callback?)start()، وAnimationEnd لانتظارهيشغّله (من جديد). يُنجَز، ويستدعي callback، مرة واحدة للشجرة كلها بالقيمة { finished }: هل اكتملت لا أنها أُوقفت.
stop()stop()يوقفه حيث هو.
reset()reset()يوقفه ويعيد قيمه إلى ما كانت عليه عند بدئه.
let ys: Vec<AnimatedValue> = (0..4).map(|_| animated_value(0.0)).collect();
let hop = Animation::stagger(
    80.0,
    ys.iter().map(|y| {
        Animation::sequence([
            Animation::timing(y, -36.0, Timing::new(220.0, Easing::Curve(Curve::Quad, EaseMode::Out))),
            Animation::spring(y, 0.0, Spring { stiffness: 300.0, damping: 12.0, ..Spring::default() }),
        ])
    }),
);
let run = hop.start();
spawn(async move { log::info!("{}", if run.await { "done" } else { "stopped" }) });

#Easing

type Easing =
  | "linear" | "ease" | "easeIn" | "easeOut" | "easeInOut"
  | [number, number, number, number]   // cubic Bézier x1, y1, x2, y2
  | EasingCurve
  | EasingSteps;                     // Easing.steps(…)

const Easing: {
  linear: Easing;
  ease: Easing;
  bezier(x1: number, y1: number, x2: number, y2: number): Easing;
  steps(count: number, position?: "jump-start" | "jump-end" | "jump-none" | "jump-both" | "start" | "end"): EasingSteps;
  in(e: Easing): Easing;
  out(e: Easing): Easing;
  inOut(e: Easing): Easing;
  sample(easing: Easing, count: number): number[];
  quad: EasingCurve;
  cubic: EasingCurve;
  poly(n?: number): EasingCurve;
  sin: EasingCurve;
  circle: EasingCurve;
  exp: EasingCurve;
  elastic(bounciness?: number): EasingCurve;
  back(s?: number): EasingCurve;
  bounce: EasingCurve;
};

الـ easing بيانات، وليس دالة من التطبيق: يقيّمه runtime بشكل native. في Rust هو enum Easing: Linear وEaseIn وEaseOut وEaseInOut وBezier(x1, y1, x2, y2) وSteps(count, StepPosition) وCurve(curve, mode)، مع Curve (Quad وCubic وPoly(n) وSin وCircle وExp وElastic(bounciness) وBack(s) وBounce) وEaseMode (In وOut وInOut). لرسم أحدها، يعطي Easing.sample(easing, count) (Rust: nsh_core::element::sample_easing) قيمه عند count من النقاط المتباعدة بالتساوي من 0 إلى 1؛ ويعطي Animated.sampleSpring وAnimated.sampleDecay (sample_spring وsample_decay) قيم spring أو decay، قيمة لكل إطار (عند fps، وهو 60 افتراضيًا) من from (0) حتى ينتهي. وتأتي الثلاثة من الشيفرة native التي تعمل بها الرسوم المتحركة. وفي أي مكان يُقبل فيه Easing في TSX يمكنك تمرير اسم ("easeOut")، أو منحنى Bézier تكعيبي بأربعة أرقام، أو منحنى من الكائن Easing.

المنحنىRustالشكل
quadCurve::Quadt²
cubicCurve::Cubict³
poly(n)Curve::Poly(n)tⁿ (القيمة الافتراضية لـ n هي 2)
sinCurve::Sinجيبي
circleCurve::Circleدائري
expCurve::Expأُسّي
elastic(bounciness)Curve::Elastic(bounciness)تجاوز نابضي (القيمة الافتراضية لـ bounciness هي 1)
back(s)Curve::Back(s)يتراجع قبل الانطلاق؛ s هو مقدار التجاوز (الافتراضي 1.70158)
bounceCurve::Bounceيرتدّ عند النهاية

Easing.steps(count, position) هو steps() في CSS: تقفز القيمة بين count من المستويات بدل أن تتحرك (sprite إطارًا بإطار، عقرب يدقّ). position يحدد مواضع القفزات: "jump-end" (الافتراضي، وكذلك "end")، أو "jump-start" ("start")، أو "jump-none" أو "jump-both"؛ Rust: Easing::Steps(count, StepPosition::JumpEnd) وJumpStart وJumpNone وJumpBoth.

المنحنيات بصيغة “in” (بطيئة في البداية). Easing.out(curve) يعكس المنحنى، وEasing.inOut(curve) يجعله متماثلًا، وEasing.in(curve) يبقيه كما هو؛ وفي Rust يكون ذلك هو EaseMode في Easing::Curve.

let _ = value.timing(1.0, 600.0, Easing::Curve(Curve::Back(1.70158), EaseMode::Out));
let _ = value.timing_with(1.0, Timing { easing: Easing::Bezier(0.2, 0.0, 0.0, 1.0), ..Timing::default() });
let _ = value.timing_with(1.0, Timing { easing: Easing::EaseOut, ..Timing::default() });

#LayoutAnimation

const LayoutAnimation: {
  configureNext(config: LayoutAnimationConfig, onEnd?: () => void): void;
  create(duration: number, type?: LayoutAnimationType, property?: LayoutAnimationProperty): LayoutAnimationConfig;
  Types: { easeInEaseOut; spring; linear; easeIn; easeOut; keyboard };
  Properties: { opacity; scaleXY };
  Presets: { easeInEaseOut: LayoutAnimationConfig; linear: LayoutAnimationConfig; spring: LayoutAnimationConfig };
  easeInEaseOut(onEnd?: () => void): void;
  linear(onEnd?: () => void): void;
  spring(onEnd?: () => void): void;
};

interface LayoutAnimationConfig {
  duration: number;                // ms
  create?: LayoutAnimationAnimation;    // views inserted
  update?: LayoutAnimationAnimation;    // views moved or resized
  delete?: LayoutAnimationAnimation;    // views removed
}

interface LayoutAnimationAnimation {
  type?: "easeInEaseOut" | "spring" | "linear" | "easeIn" | "easeOut" | "keyboard";
  property?: "opacity" | "scaleXY" | "scale";
  duration?: number;
  delay?: number;
  springDamping?: number;          // 0..1, default 0.7
}

يحرّك LayoutAnimation.configureNext(config) تغييرات التخطيط في الـ commit التالي بشكل native: العروض التي تتحرك أو يتغير حجمها تنزلق إلى إطاراتها الجديدة، والعروض الجديدة تظهر (بتلاشٍ أو بتكبير، بحسب property)، والعروض المحذوفة تختفي. استدعه قبل تغيير الحالة الذي يغيّر التخطيط مباشرة. Rust: animate_layout(LayoutAnimation)، مع presets الخاصة بـ nsh_core::element::LayoutAnimation أو انتقالاتها create وupdate وdelete.

العضوRustالوصف
configureNext(config, onEnd?)animate_layout(config)يحرّك تغييرات الـ commit التالي؛ ويعمل onEnd عند الانتهاء.
create(duration, type?, property?)LayoutAnimation::all(LayoutTransition::new(curve, duration))إعداد يستخدم المنحنى نفسه للمراحل الثلاث.
Presets.easeInEaseOutLayoutAnimation::EASE_IN_EASE_OUT300 ms، والعروض تتلاشى دخولًا وخروجًا.
Presets.linearLayoutAnimation::LINEAR500 ms بسرعة ثابتة.
Presets.springLayoutAnimation::SPRING700 ms؛ الإطارات تنتفض كالنابض (damping 0.4)، والعروض تتلاشى خطيًا.
easeInEaseOut(onEnd?) وlinear(onEnd?) وspring(onEnd?)animate_layout(LayoutAnimation::…)اختصارات لـ configureNext(Presets.…).
Types وPropertiesLayoutCurve وLayoutPropertyالأسماء، كثوابت.
let open = signal(false);
let toggle = move || {
    animate_layout(LayoutAnimation::SPRING);
    open.update(|o| *o = !*o);
};

view()
    .child(text("Details").on_press(toggle))
    .child(when(move || open.get(), || text("Shown with a spring.")))

#Device

تعرّف device API التطبيقَ بالجهاز وبيئته. تستدعي شيفرة Rust دوال nsh_core::prelude (window() وdark() وfont_scale() وlocale() وlifecycle() وos() وidiom() وpixel_ratio() وset_color_scheme() و…)؛ وتستوردها TSX من nsh. حجم النافذة ونظام الألوان ومقياس الخط ونسبة البكسل واللغة المحلية (locale) وحالة التطبيق كلها تفاعلية: اقرأها في binding (إغلاق closure يُمرَّر إلى عنصر في Rust، وJSX في TSX) أو computed أو effect، فيتحدّث كل ما يقرؤها عندما تبلّغ المنصة عن تغيير (تدوير الشاشة، تغيير حجم النافذة، التبديل إلى الوضع الداكن، تغيير حجم النص أو اللغة، الانتقال إلى شاشة بكثافة أخرى، مغادرة التطبيق والعودة إليه).

use nsh_core::{
    platform::{Appearance, Idiom, Lifecycle, Os},
    prelude::*, // window, dark, font_scale, locale, lifecycle, os, idiom, pixel_ratio, set_color_scheme, ...
};

راجع الجهاز والبيئة للاطلاع على الدليل.

#windowSize

const windowSize: Accessor<Size>;

حجم السطح بالنقاط (points): النافذة، أو NshAppView / NshView المضمَّن. تفاعلي. Rust: window() -> Size<Points>.

let wide = move || window().width >= px(600.0);

view().style_when(wide, |s| s.flex_row())

#colorScheme

const colorScheme: Accessor<"light" | "dark">;

المظهر المعروض: مظهر النظام، ما لم يضبط التطبيق مظهره الخاص (nsh.json "appearance"، setColorScheme). تفاعلي. Rust: dark() -> bool؛ وألوان السمة (AdaptiveColor) ونسخة style ‏.dark(…) تتبعه دون قراءته. كما يتبعه متغيّر dark: في Tailwind (انظر Tailwind).

view().flex_1().bg(move || if dark() { black() } else { white() })

#fontScale

const fontScale: Accessor<number>;

حجم النص المفضّل لدى المستخدم نسبةً إلى الافتراضي (1 هو الحجم الافتراضي؛ Dynamic Type في iOS، وحجم الخط في Android)، أو حجم النص الذي يضبطه التطبيق المضيف بدلًا منه. تفاعلي. Rust: font_scale() -> f32. النص يتدرّج معه أصلًا ما لم allowFontScaling={false}؛ اقرأه عندما ينبغي أن تتبعه أحجام أخرى، مثل الأيقونات بجوار النص.

#locale

const locale: Accessor<string>;

لغة المستخدم المحلية بصيغة وسم BCP 47 ("en-US"، "zh-Hans-CN")، أو اللغة التي يضبطها التطبيق المضيف بدلًا منها. تفاعلي. Rust: locale() -> Locale (locale().as_str() هو الوسم). ينسّق Intl وفقها افتراضيًا؛ اقرأها لاختيار نصوص ترجماتك الخاصة (الجهاز والبيئة).

#lifecycle

const lifecycle: Accessor<"active" | "inactive" | "background">;

هل التطبيق قيد الاستخدام، بمصطلحات AppState في React Native: "active" في المقدمة ويستقبل الإدخال، "inactive" وهو ظاهر دون التركيز (يملكه تطبيق أو نافذة أخرى، أو يعرض النظام شيئًا فوقه، كمبدّل التطبيقات أو مكالمة واردة)، "background" وهو غير ظاهر (في الخلفية، أو مصغّر، أو تبويب متصفح مخفي). والتطبيق المعروض داخل تطبيق native يتبع حالة ذلك التطبيق. تفاعلي. Rust: lifecycle() -> Lifecycle (Lifecycle::Active وInactive وBackground).

اقرأه في effect لإيقاف العمل الذي لا يراه أحد واستئنافه عند عودة التطبيق:

effect(move || {
    if lifecycle() == Lifecycle::Background {
        feed.pause();
    } else {
        feed.resume();
    }
});

#setColorScheme

function setColorScheme(scheme: "light" | "dark" | "system"): void;

يعرض التطبيق بـ scheme مهما كان مظهر النظام، فورًا: colorScheme() وفئات dark: وسمة المكوّنات وعناصر التحكم في المنصة (التنبيهات، والنمط الافتراضي لشريط الحالة) تتبعه؛ و"system" يعيده إلى اتباع النظام. nsh.json "appearance" يضبطه منذ البداية. Rust: set_color_scheme(Appearance::Dark) (Appearance::Light وAppearance::System).

#platform

const platform: Platform;

interface Platform {
  readonly os: "ios" | "android" | "macos" | "windows" | "linux" | "web";
  readonly version: string;
  readonly idiom: "phone" | "pad" | "mac" | "tv" | "desktop";
  readonly desktop: boolean;
  readonly catalyst: boolean;
  select<T>(spec: PlatformSelectSpec<T>): T | undefined;
}

المنصة التي يعمل عليها التطبيق، وهي ثابتة طوال مدة التشغيل.

العضوRustالوصف
osos() -> Os (os().name() هو نص TS)نظام التشغيل. تُبلغ iOS ومحاكي iOS وMac Catalyst عن "ios"، والمتصفحات عن "web".
versionos_version() -> (u16, u16)في iOS وأنظمة سطح المكتب: إصدار نظام التشغيل ("17.5")؛ في Android: مستوى API ("35")؛ في الويب: "0.0". يعطي Rust الإصدار الرئيسي والفرعي، أو مستوى API و0 على Android.
idiomidiom() -> Idiomنوع الجهاز: "desktop" تحديدًا حيث يكون desktop هو true؛ وفي غير ذلك "phone"، أو "pad" (iPad، أو جهاز Android لوحي، أو Mac Catalyst بنمط iPad)، أو "mac" (Mac Catalyst بنمط Mac في UIKit، وهو نمط جوال)، أو "tv" (Apple TV وAndroid TV).
desktopis_desktop() -> boolينطبق سلوك سطح المكتب: macOS وWindows وLinux والمتصفحات ذات الفأرة والنافذة العريضة. وتتبعه كلها خصائص desktop={{…}} وselect({ desktop }) وidiom "desktop". يشغّل Mac Catalyst الواجهة الخلفية لـ iOS، لذا فهو جوال: تنطبق عليه خصائص ios={{…}}. أما isAvailable في nsh/desktop فهو الفحص الأضيق لواجهات النوافذ والقوائم فيه، وهي متوفرة في macOS وWindows وLinux.
catalystis_catalyst() -> boolيعمل التطبيق ضمن Mac Catalyst، بنمط Mac أو iPad (os هو "ios").
select(spec)match os() { … }القيمة الخاصة بهذه المنصة (انظر أدناه).
let columns = if is_desktop() { 3 } else { 1 };

#platform.select

interface PlatformSelectSpec<T> {
  ios?: T;
  android?: T;
  macos?: T;
  windows?: T;
  linux?: T;
  web?: T;
  mobile?: T;   // platform.desktop false: iOS (Mac Catalyst included), Android, phone and tablet browsers
  desktop?: T;  // platform.desktop true: macOS, Windows, Linux, desktop browsers
  native?: T;   // every platform but the web
  default?: T;
}

تعيد القيمة الخاصة بنظام التشغيل الحالي (ios، android، web، …)، وإلا desktop أو mobile (بحسب platform.desktop)، وإلا native (كل المنصات عدا الويب)، وإلا default، وإلا undefined. يعتمد كود Rust على المطابقة مع os() (ومع is_desktop() لـ mobile / desktop).

use nsh_core::platform::Os;

let card = view().p_4();
let card = match os() {
    Os::Ios => card.shadow([BoxShadow {
        offset_y: px(2.0),
        blur: px(8.0),
        color: rgba(0x00000026).into(),
        ..BoxShadow::default()
    }]),
    Os::Android => card.elevation(px(4.0)),
    _ => card,
};

#pixelRatio

const pixelRatio: Accessor<number>;

عدد بكسلات الجهاز لكل نقطة (مثل 3 في معظم أجهزة iPhone). وهي تفاعلية: تتغير عندما تنتقل النافذة إلى شاشة ذات كثافة مختلفة (على Mac Catalyst وأنظمة سطح المكتب أو شاشة خارجية)، وعندما يغيّر إعداد حجم العرض في Android الكثافة. في Rust: pixel_ratio() -> f32، وround_to_nearest_pixel(points) لطول يُقاس بوحدات بكسلات الجهاز الكاملة؛ وفي TSX يكون هذا الطول Math.round(x * pixelRatio()) / pixelRatio().

let hairline = move || px(1.0 / pixel_ratio());

#openUrl

function openUrl(url: string): Promise<OpenUrlResult>;

interface OpenUrlResult {
  opened: boolean;
}

تفتح url خارج التطبيق: صفحة ويب في المتصفح، ورابط mailto: في تطبيق البريد، ورابط tel: في تطبيق الهاتف، وأي مخطط سجّله تطبيق آخر (maps:، وروابط lb:// الخاصة بالتطبيق المضيف). وتُحلّ بالقيمة { opened }، أي هل فتحها شيء ما: false عندما لا يعالج أي تطبيق المخطط. في Rust: open_url(url) من nsh_core::prelude، وهي نتيجة يُنتظر اكتمالها داخل مهمة (أو تُهمل)، وهي نفس bool.

button()
    .child(text("Contact us"))
    .on_press(|| {
        spawn(async {
            if !open_url("mailto:help@example.com").await {
                log::warn!("no mail app");
            }
        });
    })
  • يجب أن يكون الرابط مطلقًا (له مخطط). الروابط التي قد تنفّذ كودًا أو تصل إلى بيانات التطبيق نفسه، javascript: وdata: وblob: وfile: وabout: وasset:، ترفض الوعد بـ TypeError، وكذلك العناوين النسبية. لا ترمي openUrl استثناءً أبدًا: كل فشل يرفض الوعد.
  • يحتاج التطبيق الذي يعلن القدرات إلى openUrl: "openUrl": true لكل المخططات، أو "openUrl": { "schemes": ["https", "mailto"] } للمخططات المدرجة؛ وترفض الروابط الأخرى بـ Error تكون code فيه "ERR_ACCESS_DENIED"، وpermission "openUrl" وresource الرابط (انظر الصندوق الرملي والقدرات). في Rust، تُجيب open_url بـ false في هذه الحالات وتسجّل السبب.
  • يرى التطبيق المضيف الرابط أولًا ويمكنه توجيهه عبر موجّهه الخاص بدلًا من ذلك (Swift NshAppView.onOpenURL، Java NshView.setOpenUrlHandler، Rust AppView::host().handle_open_url؛ انظر iOS وAndroid). وتكون opened عندئذ true.
  • تفتحه كل منصة بفاتحها الخاص: UIApplication.open في iOS (تطبيق المضيف عند التضمين)، وintent من نوع ACTION_VIEW في Android، وNSWorkspace في macOS، وShellExecuteW في Windows، وGtkUriLauncher في Linux، وopen_url من GPUI، وعلامة تبويب جديدة في الويب. أما nsh test فتسجّل الروابط بدلًا من فتحها.
  • يفتح TextView روابطه بهذه الطريقة باستخدام links="open".

#اتجاه التخطيط

يتبع اتجاه التخطيط لغة المستخدم: في اللغات التي تُكتب من اليمين إلى اليسار (العربية والعبرية وغيرهما) يُخطَّط التطبيق من اليمين إلى اليسار، وتبدأ الصفوف من اليمين، وتعني خصائص الأنماط left / right (marginLeft، paddingRight، …) البداية والنهاية. تُطبَّق التغييرات فورًا دون إعادة تشغيل.

يحدد نمط direction في العرض ("ltr" أو "rtl" أو "inherit"، وهو الافتراضي) اتجاه شجرته الفرعية؛ وفي Rust: .direction(LayoutDirection::..).

view().direction(LayoutDirection::Rtl).flex_row().gap_2().child(text("First")).child(text("Second"))

#ذات صلة

#PanResponder

مستجيبات السحب (Pan responders) هي إيماءات سحب مبنية فوق نظام مستجيب الإيماءات: يستحوذ العنصر على لمسات الإيماءة، ثم يسمعها وحده مع حالة الإيماءة، التي تتتبع مركز اللمسات والمسافة المقطوعة والسرعة نيابةً عنك. يستحوذ كود Rust عليها بـ .claim_touches(TouchClaim::…) ويسمعها بمستمعي .on_responder_* الذين يحمل ResponderEvent فيهم PanGesture، وكل ذلك في nsh_core::prelude؛ أما TSX فيستورد PanResponder من nsh، وتتلقى دوال onPanResponder… فيه حدث اللمس وحالة الإيماءة.

use nsh_core::prelude::*; // TouchClaim, ClaimRequest, ResponderEvent, PanGesture

#PanResponder.create

const PanResponder: { create(config: PanResponderCallbacks): PanResponderInstance };

interface PanResponderInstance {
  panHandlers: TouchProps;
}

تنشئ مستجيب سحب. وزّع panHandlers الخاصة به على العرض الذي يجب أن يستقبل الإيماءة. في Rust، يستحوذ العرض على اللمسات ويستمع بنفسه:

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    let pos = signal((0.0_f32, 0.0_f32));
    let start = stored((0.0_f32, 0.0_f32));
    view().flex_1().child(
        view()
            .size(px(64.0))
            .rounded(px(32.0))
            .bg(rgb(0x6366f1))
            .style_with(move |s| {
                let (x, y) = pos.get();
                s.translate_x(px(x)).translate_y(px(y))
            })
            .claim_touches(TouchClaim::on_start())
            .on_responder_grant(move |_: &ResponderEvent| {
                start.replace(pos.get_untracked());
            })
            .on_responder_move(move |e: &ResponderEvent| {
                let (x, y) = start.get();
                pos.set((x + e.gesture.delta.x.0, y + e.gesture.delta.y.0));
            })
            .on_responder_release(move |e: &ResponderEvent| {
                log::info!("released at {:.0} pt/s", e.gesture.vx);
            }),
    )
}

#دوال الاستدعاء

تتلقى كل دالة استدعاء حدث اللمس (GestureResponderEvent، من nsh) وحالة الإيماءة. في Rust، يقرر الاستحواذ عبر .ask(|request: &ClaimRequest| …)، مستدعيًا request.claim() لأخذ اللمسات (وبدون ask يأخذها دائمًا)، ويتلقى كل مستمع ResponderEvent تكون gesture فيه هي الحالة.

type PanHandler<R = void> = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => R;
دالة الاستدعاءRustتعيدتُستدعى عندما
onStartShouldSetPanResponder.claim_touches(TouchClaim::on_start())booleanيلمس إصبع الشاشة: أعد true لتصبح المستجيب. يُسأل بدءًا من العرض الملموس نحو الخارج.
onStartShouldSetPanResponderCaptureTouchClaim::on_start().capture()booleanالأمر نفسه، لكن يُسأل من الخارج إلى الداخل قبل مرحلة الفقاعة (أب يأخذ الإيماءة من أبنائه).
onMoveShouldSetPanResponderTouchClaim::on_move().ask(…)booleanيتحرك إصبع: أعد true لتصبح المستجيب (مثلًا فقط بعد Math.abs(g.dx) > 10).
onMoveShouldSetPanResponderCaptureTouchClaim::on_move().capture()booleanالأمر نفسه، في مرحلة الالتقاط.
onPanResponderGrant.on_responder_grant(…)هذا العرض هو المستجيب الآن: تبدأ الإيماءة.
onPanResponderReject.on_responder_reject(…)عرض آخر هو المستجيب ولم يتخلَّ عنها.
onPanResponderStart.on_responder_start(…)لمس إصبع الشاشة أثناء الاستجابة.
onPanResponderMove.on_responder_move(…)تحركت الأصابع.
onPanResponderEnd.on_responder_end(…)رُفع إصبع أثناء الاستجابة.
onPanResponderRelease.on_responder_release(…)رُفع آخر إصبع: انتهت الإيماءة.
onPanResponderTerminate.on_responder_terminate(…)أخذ عرض آخر (أو إيماءة أصلية، مثل عرض التمرير) اللمسات.
onPanResponderTerminationRequest.keep_touches(true) يحتفظ بهاbooleanيريد عرض آخر اللمسات: أعد false للاحتفاظ بها (الافتراضي true).
onShouldBlockNativeResponderTouchClaim::block_native(…)booleanهل يُمنع عرض التمرير الأصلي من أخذ اللمسات أثناء الاستجابة (الافتراضي true).

#PanResponderGestureState

interface PanResponderGestureState {
  stateID: number;
  moveX: number;               // latest centroid of the touches (surface points)
  moveY: number;
  x0: number;                  // the centroid when the responder was granted
  y0: number;
  dx: number;                  // distance moved since the grant
  dy: number;
  vx: number;                  // velocity, points per second
  vy: number;
  numberActiveTouches: number;
}

في Rust: PanGesture، مع النقاط على شكل Point<Points>.

الحقلRustالوصف
stateIDيحدد الإيماءة.
moveX، moveYpositionآخر موضع لمركز اللمسات، بنقاط السطح.
x0، y0originموضع المركز عند منح الاستجابة.
dx، dydeltaالمسافة المقطوعة منذ المنح.
vx، vyvx، vyالسرعة بالنقاط في الثانية.
numberActiveTouchesactive_touchesعدد الأصابع الملامسة.

#داخل عروض التمرير

بينما يكون مستجيب السحب هو المستجيب، لا تتمرر عروض التمرير الأصلية المحيطة به (onShouldBlockNativeResponder افتراضيًا true). ولكي تتعايش إيماءة سحب أفقية مع التمرير العمودي، لا تصبح المستجيب إلا عند الحركات الأفقية:

view()
    .claim_touches(TouchClaim::on_move().ask(|request: &ClaimRequest| {
        let d = request.event.gesture.delta;
        if d.x.0.abs() > 10.0 && d.x.0.abs() > d.y.0.abs() {
            request.claim();
        }
    }))
    .on_responder_move(move |e: &ResponderEvent| offset.set(e.gesture.delta.x.0))
    .on_responder_release(move |e: &ResponderEvent| {
        if e.gesture.delta.x.0.abs() > 120.0 { remove() } else { offset.set(0.0) }
    })
    .on_responder_terminate(move |_: &ResponderEvent| offset.set(0.0))

بالنسبة إلى السحب الذي تتعرف عليه المنصة، يقرر TouchClaim::on_pan(PanActivation::horizontal(10.0, 10.0)) على الخيط الرئيسي أثناء حركة الأصابع. ويمكن أيضًا استخدام خصائص المستجيب الأساسية (onStartShouldSetResponder، onResponderGrant، onTouchStart، …) مباشرة على أي عنصر؛ انظر العناصر.

#Clipboard

تقرأ واجهة الحافظة (Clipboard) حافظة النظام وتكتب فيها: النص في كل مكان (UIPasteboard في iOS، ClipboardManager في Android، NSPasteboard وGDK وحافظة Win32 في أنظمة سطح المكتب، navigator.clipboard في الويب)، والصور في iOS وأنظمة سطح المكتب والويب، والملفات المنسوخة في مدير الملفات في أنظمة سطح المكتب وMac Catalyst. يستدعي كود Rust دوال nsh_core::prelude (set_clipboard، clipboard_string، …)؛ أما TSX فيستورد Clipboard من nsh.

use nsh_core::prelude::*; // set_clipboard, clipboard_string, clipboard_image, ...
use nsh_core::element::ClipboardImage;
const Clipboard: {
  writeText(text: string): Promise<void>;
  readText(): Promise<string>;
  hasText(): Promise<boolean>;
  writeImage(src: string): Promise<void>;
  readImage(): Promise<ClipboardImage | null>;
  hasImage(): Promise<boolean>;
  readFiles(): Promise<string[]>;
};

interface ClipboardImage {
  url: string; // a URL that Image shows and fetch reads
  size: PixelSize; // in pixels
}
TSRust (nsh_core::prelude)
await Clipboard.writeText(text)set_clipboard(text)
await Clipboard.readText()clipboard_string().await -> String
await Clipboard.hasText()clipboard_has_string().await -> bool
await Clipboard.writeImage(src)set_clipboard_image(src)
await Clipboard.readImage()clipboard_image().await -> Option<ClipboardImage>
await Clipboard.hasImage()clipboard_has_image().await -> bool
await Clipboard.readFiles()clipboard_files().await -> Vec<String>

تعيد عمليات القراءة في Rust نتائج يُنتظر اكتمالها داخل مهمة (spawn، أو دالة في وحدة async)؛ وClipboardImage هي nsh_core::element::ClipboardImage، مع url وsize (وهي Size<DevicePixels>).

تمر الصور عبر الحافظة في iOS وأنظمة سطح المكتب والويب. أما في Android فتحمل الحافظة النص فقط: تُرفض writeImage و readImage بـ Error تكون code فيه "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM"، وتُحلّ hasImage بالقيمة false. في Rust، تترك set_clipboard_image الحافظة كما هي هناك، وتُجيب clipboard_image بـ None.

يستطيع المستخدم أيضًا النسخ بنفسه من نص قابل للتحديد؛ وواجهة الحافظة مخصصة للنسخ من الكود، مثل زر “نسخ”.

#Clipboard.writeText

writeText(text: string): Promise<void>;

تضع text في الحافظة، مستبدلة ما كان فيها. في Rust: set_clipboard(text).

use nsh_core::prelude::*;

fn copy_button(code: Signal<String>) -> impl IntoElement {
    let copied = signal(false);
    view()
        .role(AccessibilityRole::Button)
        .on_press(move || {
            set_clipboard(code.get());
            copied.set(true);
        })
        .child(text(move || if copied.get() { "Copied" } else { "Copy" }))
}

#Clipboard.readText

readText(): Promise<string>;

تُحلّ بنص الحافظة، أو بـ "" عندما لا تحتوي على نص. في Rust: clipboard_string(). في iOS، قد تُظهر قراءة الحافظة إشعار اللصق من النظام (“App pasted from …”)، وفي Android 10 وما بعده لا يستطيع القراءة إلا التطبيق الموجود في المقدمة. اقرأها عندما يطلب المستخدم اللصق، وليس في كل شاشة.

spawn(async move {
    let pasted = clipboard_string().await;
    if !pasted.is_empty() {
        code.set(pasted.trim().to_string());
    }
});

#Clipboard.hasText

hasText(): Promise<boolean>;

تُحلّ بما إذا كانت الحافظة تحتوي على نص، دون قراءته: لا إشعار لصق في iOS. في Rust: clipboard_has_string(). استخدمها لتقرر هل تُظهر زر “لصق”. في الويب، لا تعرف المتصفحات إلا بالقراءة، لذا قد يُطلب من المستخدم إذن.

let can_paste = signal(false);
spawn(async move { can_paste.set(clipboard_has_string().await) });

#Clipboard.readImage

readImage(): Promise<ClipboardImage | null>;

تُحلّ بصورة الحافظة، أو بـ null عندما لا تحتوي على صورة: لقطة شاشة ملصقة، أو صورة نُسخت في متصفح أو محرر صور. في Rust: clipboard_image()، وNone عندما لا تحتوي على صورة. url هو رابط الصورة (PNG، أو JPEG أو GIF أو WebP أو BMP الخاصة بالحافظة) الذي يعرضه Image بوصفه src وتقرؤه fetch: fetch(url) تعطي بايتاتها، لرفعها. وsize (width وheight) هو حجمها بالبكسل، كما فُكّ ترميزها. وكما في readText، اقرأها عندما يطلب المستخدم اللصق. تُقرأ الصور حتى 48 ميغابايت (مرمّزة) كي تتسع الصورة في ذاكرة التطبيق؛ وترفض الصورة الأكبر بـ Error تكون code فيه "ERR_NSH_QUOTA_EXCEEDED" (وتذكر رسالته حجم الصورة والحد الأقصى)، بينما تُحلّ hasImage بـ true. في Rust: تُجيب clipboard_image() بـ None في هذه الحالة.

use base64::Engine as _;
use nsh_core::{element::ClipboardImage, prelude::*};

fn attachments() -> impl IntoElement {
    let attachments = signal(Vec::<ClipboardImage>::new());
    let paste = move || {
        spawn(async move {
            if let Some(image) = clipboard_image().await {
                attachments.update(|list| list.push(image));
            }
        });
    };
    view()
        .flex_row()
        .gap_2()
        .child(text("Paste").on_press(paste))
        .child(each(
            attachments,
            |shot: &ClipboardImage| shot.url.clone(),
            |shot, _| image(move || shot.with(|s| s.url.clone())).size(px(64.0)).rounded(px(8.0)),
        ))
}

async fn upload(image: &ClipboardImage) -> Result<HttpResponse, String> {
    let (_, data) = image.url.split_once(',').ok_or("not a data: URL")?;
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(data)
        .map_err(|e| e.to_string())?;
    let request = HttpRequest::post("https://example.com/upload", bytes).header("Content-Type", "image/png");
    fetch(request).await
}

يربط تطبيق على نمط الدردشة، يلصق فيه المستخدمون لقطات الشاشة في مربع الكتابة، اختصار اللصق ما دام حقل الإدخال في التركيز، ويأخذ الصورة إن وجدت:

use nsh_core::keymap::When;

on_key_with("mod+v", KeyOptions { when: When::Always, ..KeyOptions::default() }, move |_| {
    spawn(async move {
        if clipboard_has_image().await {
            paste().await;
        } else {
            insert_text(&clipboard_string().await);
        }
    });
});

#Clipboard.hasImage

hasImage(): Promise<boolean>;

تُحلّ بما إذا كانت الحافظة تحتوي على صورة، دون قراءتها: لا إشعار لصق في iOS. في Rust: clipboard_has_image(). في Android تُحلّ بالقيمة false.

#Clipboard.writeImage

writeImage(src: string): Promise<void>;

تضع صورة src، وهو رابط data: بترميز base64 أو url الذي أعطته readImage، في الحافظة، مستبدلة ما فيها. في Rust: set_clipboard_image(src). في Windows تقبل PNG، وهو التنسيق الذي تقرؤه المتصفحات وOffice وأداة القص. في Android تُرفض بـ "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM".

set_clipboard_image(image.url.clone());

#Clipboard.readFiles

readFiles(): Promise<string[]>;

تُحلّ بروابط file: للملفات والمجلدات المنسوخة في مدير الملفات (Finder وExplorer وFiles)، بترتيبها، أو بـ []: وهي نفس الروابط التي يعطيها إسقاط الملفات. في Rust: clipboard_files(). حلّل الرابط للحصول على مساره.

let paths: Vec<std::path::PathBuf> = clipboard_files()
    .await
    .iter()
    .filter_map(|file| url::Url::parse(file).ok()?.to_file_path().ok())
    .collect();

#Picker

يعرض Picker API أداة اختيار الصور في النظام ويمنح التطبيق الصور التي يختارها المستخدم: أداة Photos picker على iOS (PHPickerViewController)، وأداة Photo Picker على Android، ولوحة فتح أو مربع حوار ملفات على أنظمة سطح المكتب، وحقل إدخال ملف (file input) في المتصفح. على iOS وAndroid تعمل الأداة خارج التطبيق وتسلّم فقط ما يختاره المستخدم، ولذلك لا تحتاج إلى إذن الوصول إلى مكتبة الصور. تصل كل صورة مختارة على شكل File (في TSX) أو PickedFile (في Rust) تبقى بايتاتها في الطبقة الأصلية (native): اعرضها باستخدام URL.createObjectURL(file) داخل Image، وارفعها باستخدام fetch وFormData. يستدعي كود Rust الدالة pick_images من nsh_core::prelude؛ أما TSX فيستورد Picker من nsh.

use nsh_core::picker::{PickRequest, PickedFile};
use nsh_core::prelude::*; // pick_images
const Picker: {
  pickImages(options?: PickImagesOptions): Promise<PickedImage[]>;
};

interface PickImagesOptions {
  multiple?: boolean; // let the user pick several (false: one)
  limit?: number; // at most this many when multiple (up to 100)
  maxDimension?: number; // scale images whose longer side is longer, in pixels
  quality?: number; // JPEG quality from 0 to 1 (0.9)
  format?: "compatible" | "current"; // "compatible": HEIC and the like as JPEG
}

interface PickedImage {
  file: File; // name, type, size in bytes and the bytes
  size: PixelSize; // upright
}
TSRust
await Picker.pickImages(options)pick_images(request).await -> Vec<PickedFile>
multiple, limitPickRequest::new(multiple, limit) (limit: القيمة 0 تعني حتى 100)
maxDimensionrequest.max_dimension = Some(DevicePixels(2048))
qualityrequest.quality = Some(Percentage(0.8))
formatrequest.format = PickFormat::Current
image.file.name, .type, .sizefile.name, file.mime, file.data.len()
image.sizefile.size (Size<DevicePixels>)

#Picker.pickImages

pickImages(options?: PickImagesOptions): Promise<PickedImage[]>;

تعرض أداة الاختيار ثم تُنجَز بالصور التي اختارها المستخدم، بالترتيب الذي اختارها به، أو بالقيمة [] عندما يلغي المستخدم. في Rust: pick_images(request)، وهي نتيجة تُنتظر داخل مهمة (task) (spawn، أو دالة وحدة (module) من async). pickImages لا ترمي استثناءً أبدًا: الخيارات خارج النطاق (limit: 0، أو quality خارج المجال من 0 إلى 1) ترفض الوعد (promise) بـ RangeError، والاختيار الذي لا تمنحه صلاحيات التطبيق (capabilities) يُرفض بـ ERR_ACCESS_DENIED (انظر أدناه). الإلغاء هو جواب المستخدم وليس فشلًا: فهو يُنجز بالقيمة [].

تصل الصور جاهزة للإرسال:

  • صيغ يقبلها كل متصفح وكل خادم. تصل صور JPEG وPNG وGIF وWebP كما هي؛ أما الصورة المخزّنة بصيغة أخرى (HEIC على أجهزة iPhone، وHEIF، وTIFF) فتصل بصيغة JPEG. format: "current" تُبقي الصيغة المخزّنة.
  • تصغير أصلي (native). مع maxDimension، تُصغَّر الصورة التي يزيد ضلعها الأطول على هذا الحد لتناسبه، وتُعدَّل لتكون قائمة، ثم تُرمَّز من جديد: PNG بصيغة PNG، وأي صيغة أخرى بصيغة JPEG بجودة quality. صور الهواتف بدقة 12 إلى 48 ميغابكسل؛ وتصغيرها إلى 2048 بكسل قبل الرفع يجعل حجمها بضع مئات من الكيلوبايتات. كما يعيد quality ترميز صور JPEG.
  • ما تعرضه. size هو حجم الصورة بالبكسل كما تظهر، بعد تطبيق اتجاه الكاميرا. الصورة التي يعيد NSH ترميزها لا تحمل أي بيانات وصفية (ولا الموقع)؛ أما التي تصل كما هي فتحتفظ ببياناتها.

على Windows وLinux وفي نوافذ GPUI، يعرض مربع الحوار الصيغ التي يقبلها الطلب (مع "compatible": ‏JPEG وPNG وGIF وWebP) وتصل الملفات كما هي مخزّنة وبحجمها الأصلي.

يرفق مُحرِّر الدردشة صورة، ويعرضها أثناء كتابة الرسالة، ثم يرفعها عند الإرسال:

use nsh_core::geometry::{DevicePixels, Percentage};
use nsh_core::picker::{PickRequest, PickedFile};
use nsh_core::prelude::*;

fn attach_button(attached: Signal<Option<PickedFile>>) -> impl IntoElement {
    view()
        .role(AccessibilityRole::Button)
        .accessibility_label("Choose a photo")
        .on_press(move || {
            let mut request = PickRequest::new(false, 1);
            request.max_dimension = Some(DevicePixels(2048));
            request.quality = Some(Percentage(0.85));
            let picked = pick_images(request);
            spawn(async move {
                if let Some(file) = picked.await.into_iter().next() {
                    attached.set(Some(file));
                }
            });
        })
        .child(text("Photo"))
}

async fn upload(file: &PickedFile) -> Result<HttpResponse, String> {
    let request = HttpRequest::post("https://example.com/upload", file.data.to_vec())
        .header("Content-Type", file.mime.as_str());
    fetch(request).await
}

عدة صور دفعة واحدة:

let picked = pick_images(PickRequest::new(true, 9));
spawn(async move {
    let files = picked.await;
    photos.update(|list| list.extend(files));
});

#التطبيقات المضمِّنة

التطبيق الذي يضمّن NSH ولديه أداة اختيار خاصة به (مثل أداة بألبومات التطبيق نفسه) يجيب عن عمليات الاختيار بنفسه: في Rust، يستلم PickerBackend (nsh_core::picker) الممرَّر إلى المحرك (EngineConfig::picker، أو AppView::set_picker في تطبيقات GPUI وGPUI Kit) PickRequest الخاص بكل عملية اختيار ويُرجع الملفات، ولا تُعرض أداة الاختيار الخاصة بالنظام. تُستكمل الملفات التي يُرجعها كما تُستكمل ملفات المنصة: تُقصّ إلى حد الطلب، وتُقرأ أحجامها وأنواعها وامتداداتها من بايتاتها.

#الاختبارات

nsh test يجيب عن عمليات الاختيار في التشغيل بلا واجهة (headless): app.onPick(handler) يُرجع الملفات التي يختارها المستخدم ({ name, type, data })، وبدون معالج (handler) تُلغى عملية الاختيار. انظر الاختبار.

#Speech

يحوّل Speech API ما يقوله المستخدم إلى نص (الإملاء): يبدأ التطبيق جلسة، وتستمع المنصة إلى الميكروفون، ويصل النص المفرَّغ وهو ينمو. يعتمد macOS وiOS في التعرف على إطار Speech الخاص بالنظام؛ ويسأل النظام المستخدم مرة واحدة عن الميكروفون والتعرف على الكلام. على المنصات الأخرى تنتهي الجلسة فورًا بالخطأ "unavailable"، لذا تحقق من Speech.available() قبل عرض زر الميكروفون. يستدعي كود Rust الدالة recognize_speech من nsh_core::prelude؛ أما TSX فيستورد Speech من nsh.

use nsh_core::speech::{SpeechEvent, SpeechRequest};
const Speech: {
  available(): Promise<boolean>;
  start(options?: SpeechOptions): SpeechSession;
};

interface SpeechOptions {
  /** The language spoken, a BCP 47 tag ("en-US", "zh-CN"); the user's when left out. */
  lang?: string;
  /** Results while the user speaks (true, the default), or only the final one. */
  interim?: boolean;
}

interface SpeechSession {
  on(event: "start", listener: () => void): () => void;
  on(event: "result", listener: (result: SpeechResult) => void): () => void;
  on(event: "end", listener: (end: SpeechEnd) => void): () => void;
  /** The whole transcript so far. */
  readonly text: string;
  readonly ended: boolean;
  stop(): void;
  abort(): void;
}

interface SpeechResult { text: string; isFinal: boolean }
interface SpeechEnd { text: string; error?: SpeechErrorCode }

type SpeechErrorCode =
  | "not-allowed" | "no-speech" | "audio-capture" | "network"
  | "language-not-supported" | "unavailable" | "failed";
TSXRust
await Speech.available()speech_available().await -> bool
Speech.start(options)recognize_speech(request) -> SpeechRecognition
session.on("result", ...)recognition.next().await -> Option<SpeechEvent>
session.stop() / abort()recognition.stop() / abort() (إسقاطه يُلغي الجلسة)

#Speech.start

تبدأ جلسة وتُرجعها فورًا. تُبلِّغ الجلسة بالترتيب التالي: start عندما يعمل الميكروفون ويستمع المُتعرِّف، ثم result في كل مرة يتغير فيها النص المفرَّغ (text هو دائمًا النص كاملًا حتى تلك اللحظة، لذا يعرضه التطبيق كما هو)، ثم end واحد، لا يصل بعده شيء. stop() يوقف الاستماع ويدع المُتعرِّف ينهي ما سمعه: نتيجة أخيرة مع isFinal: true، ثم end. abort() ينهي الجلسة فورًا، بلا خطأ، ويُسقط ما لم يُبلَّغ عنه. تعمل جلسة واحدة في كل مرة: بدء جلسة أخرى يُلغي الجلسة الجارية.

end يحمل آخر نص مفرَّغ، وعند فشل الجلسة يحمل error، بأسماء مطابقة لما في Web Speech API:

errorالحالة
"not-allowed"لا يسمح المستخدم أو النظام بالميكروفون أو بالتعرف على الكلام.
"no-speech"لم يُقَل شيء.
"audio-capture"تعذّر استخدام الميكروفون.
"network"يحتاج المُتعرِّف إلى الشبكة وتعذّر الوصول إليها.
"language-not-supported"لا يعرف المُتعرِّف lang.
"unavailable"لا تتعرف هذه المنصة على الكلام، أو لا تتعرف عليه الآن.
"failed"فشل المُتعرِّف لسبب آخر.

start يرمي TypeError عند تمرير خيارات من أنواع خاطئة، ويرمي RangeError عندما لا يكون lang وسم لغة صالحًا.

import { signal, Speech, type SpeechSession } from "nsh";

const transcript = signal("");
let session: SpeechSession | null = null;

function toggle() {
  if (session && !session.ended) {
    session.stop();
    return;
  }
  session = Speech.start({ lang: "en-US" });
  session.on("result", ({ text }) => transcript.set(text));
  session.on("end", ({ text, error }) => {
    if (error) console.warn("dictation ended:", error);
    else transcript.set(text);
  });
}
use nsh_core::prelude::*;
use nsh_core::speech::{SpeechEvent, SpeechRequest};

async fn dictate() -> String {
    let mut recognition = recognize_speech(SpeechRequest::default());
    let mut text = String::new();
    while let Some(event) = recognition.next().await {
        match event {
            SpeechEvent::Result { text: heard, .. } => text = heard,
            SpeechEvent::End { .. } => break,
            _ => {}
        }
    }
    text
}

#Speech.available

تُنجَز بالقيمة true حيثما يمكن تشغيل جلسة: على macOS وiOS، وفي أي مكان يتعرف فيه التطبيق المضمِّن على الكلام بنفسه.

#Info.plist

على macOS وiOS يعرض النظام سبب طلب التطبيق الميكروفون والتعرف على الكلام. nsh package وnsh run يكتبان NSMicrophoneUsageDescription وNSSpeechRecognitionUsageDescription في ملف Info.plist الخاص بالتطبيق عندما يُسمح للتطبيق باستخدام الكلام (عندما تمنح صلاحيات capabilities قيمة speech، أو لا يصرّح بأي منها).

#التطبيقات المضمِّنة

التطبيق الذي يضمّن NSH ويتعرف على الكلام بطريقته الخاصة (محرك أو خدمة خاصة به) يجيب عن الجلسات بنفسه: في Rust، يستلم SpeechBackend (nsh_core::speech) الممرَّر إلى المحرك (EngineConfig::speech) SpeechRequest الخاص بكل جلسة، وSpeechSink يبلِّغ إليه من أي خيط (thread)، ويُرجع SpeechControl الذي توقف به النواة الجلسة وتُلغيها. ولا يُستخدم مُتعرِّف المنصة حينئذٍ.

#الاختبارات

nsh test يشغّل الجلسات على مُتعرِّف وهمي في التشغيل بلا واجهة (headless): تبدأ الجلسة فورًا، وapp.hear(text) يبلِّغ عن text بوصفه النص المفرَّغ حتى الآن، وapp.endSpeech(error?) ينهيها، وapp.speechRequests() يسرد الجلسات التي بدأها التطبيق. انظر الاختبار.

#Storage

يحتفظ Storage API ببيانات التطبيق على الجهاز: سلاسل نصية بمفتاح نصي، وتبقى موجودة عند بدء التطبيق في المرة التالية. استخدمه للإعدادات والمسودات وآخر تبويب فتحه المستخدم وذاكرة مؤقتة لما أرسله الخادم، أي لأي حالة ينبغي أن تعيش أطول من عملية التطبيق. يستخدم كود Rust nsh_core::storage::Storage، ويستورد TSX Storage من nsh؛ وكلاهما يقرأ ويكتب نفس المدخلات، فالتطبيق الذي فيه مدخل Rust ومدخل TSX يرى مجموعة بيانات واحدة.

use nsh_core::storage::Storage;
const Storage: {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
  keys(): Promise<string[]>;
  clear(): Promise<void>;
};
TSRust (nsh_core::storage::Storage)
await Storage.getItem(key)Storage::get_item(key) -> Option<String>
await Storage.setItem(key, value)Storage::set_item(key, value) -> Result<(), StorageFull>
await Storage.removeItem(key)Storage::remove_item(key)
await Storage.keys()Storage::get_all_keys() -> Vec<String>
await Storage.clear()Storage::clear()

دوال Rust متزامنة لأن المدخلات في الذاكرة: استدعِها حيث تعمل المكوّنات (في أجسامها ومعالجاتها ومهامها).

لكل تطبيق تخزين خاص به، حتى 5 ميغابايت من المفاتيح والقيم. تُنفَّذ الاستدعاءات بالترتيب الذي يجريها به التطبيق، لذا فإن getItem بعد setItem ترى القيمة الجديدة دون انتظار الكتابة. تأتي القراءات من الذاكرة؛ أما الكتابات فتصل إلى القرص في الخلفية، بضع عمليات في كل مرة.

المنصةمكان حفظ البيانات
iOSLibrary/Application Support/nsh/<app id>/storage.json في حاوية التطبيق
Androidfiles/nsh/<app id>/storage.json في الدليل الخاص بالتطبيق
macOS~/Library/Application Support/<app id>/storage.json
Windows%APPDATA%\<app id>\storage.json
Linux$XDG_DATA_HOME/<app id>/storage.json (~/.local/share/…)
WeblocalStorage الخاص بالمتصفح، تحت مفاتيح تحمل اسم معرّف التطبيق (nsh:com.example.todo:…)

يُحفظ storage.json التالف باسم storage.json.corrupt بجواره، ويبدأ التطبيق بتخزين فارغ؛ أما الذي تتعذّر قراءته فتبقى تغييرات التطبيق في الذاكرة، فلا يُستبدل الملف أبدًا.

معرّف التطبيق هو id في nsh.json، وهو يسافر مع التطبيق: nsh build وnsh package يكتبانه بجوار الحزمة (bundle)، وخادم التطوير يرسله، والحزمة الموقّعة تحمل المعرّف الذي تشهد به شهادة مفتاح الإصدار. وهكذا يحتفظ كل تطبيق ببياناته الخاصة دون أي إعداد، أينما عمل؛ والتطبيق بلا معرّف يحتفظ بها تحت default. يمكن للتطبيق المضمِّن اختيار مكان آخر (انظر التضمين على iOS والتضمين على Android).

#Storage.getItem

getItem(key: string): Promise<string | null>;

تُنجَز بقيمة key، أو بـ null عندما لا تكون له قيمة. في Rust: Storage::get_item(key)، وNone عندما لا تكون له قيمة.

let theme = Storage::get_item("theme").unwrap_or_else(|| "system".into());

#Storage.setItem

setItem(key: string, value: string): Promise<void>;

تضبط key على value وتستبدل ما كان فيه. تُرفض بـ Error قيمة code فيه هي "ERR_NSH_QUOTA_EXCEEDED" عندما يتجاوز التخزين 5 ميغابايت؛ ولا تُحفظ القيمة حينئذٍ. في Rust: Storage::set_item(key, value)، التي تُرجع Err(StorageFull) حينئذٍ. القيم نصوص: احتفظ بالبيانات المهيكلة بصيغة JSON (serde_json::to_string في Rust، JSON.stringify في TSX) واقرأها من جديد (serde_json::from_str، JSON.parse).

let draft = serde_json::json!({ "title": title.get(), "body": body.get() });
if let Err(full) = Storage::set_item("draft", draft.to_string()) {
    log::warn!("{full}");
}

#Storage.removeItem

removeItem(key: string): Promise<void>;

تحذف key وقيمته. حذف مفتاح لا قيمة له لا يفعل شيئًا. في Rust: Storage::remove_item(key).

#Storage.keys

keys(): Promise<string[]>;

تُنجَز بكل المفاتيح، بالترتيب. في Rust: Storage::get_all_keys().

#Storage.clear

clear(): Promise<void>;

تحذف كل مفاتيح التطبيق، مثلًا عندما يسجّل المستخدم الخروج. في Rust: Storage::clear().

#حفظ إشارة على الجهاز

اقرأ القيمة المخزّنة عند بدء التطبيق، ثم اكتبها من جديد كلما تغيّرت الإشارة (signal):

use nsh_core::{storage::Storage, prelude::*};

#[component]
pub fn App() -> impl IntoElement {
    let saved = Storage::get_item("count").and_then(|n| n.parse().ok());
    let count = signal(saved.unwrap_or(0_u32));
    effect(move || {
        if let Err(full) = Storage::set_item("count", count.get().to_string()) {
            log::warn!("{full}");
        }
    });
    view()
        .on_press(move || count.update(|n| *n += 1))
        .child(text(move || format!("Pressed {} times", count.get())))
}

#host

host يصل إلى التطبيق الذي يضمّن NSH: التطبيق الأصلي (native) المحيط بـ NshAppView / NshView، أو على الويب الصفحة التي تشغّل المشغِّل (player). يستدعي التطبيق دوال المضيف (host) بأسمائها مع وسائط JSON، ويستمع إلى الأحداث التي يرسلها المضيف.

import { host } from "nsh";
const host: {
  readonly params: { readonly [key: string]: unknown };
  post(name: string, data?: unknown): void;
  available(): boolean;
  call(name: string, args?: unknown, options?: { timeout?: number }): Promise<unknown>;
  on(event: string, listener: (payload: unknown) => void): () => void;
};

#host.available

available(): boolean;

هل للمنصة مضيف يجيب عن الاستدعاءات. على الويب تكون true عندما تمنح الصفحة المشغِّل مضيفًا (المضيف على الويب).

import { Text } from "nsh";
import { host } from "nsh";

<Text>{host.available() ? "Embedded" : "Standalone"}</Text>;

#host.call

call(name: string, args?: unknown, options?: { timeout?: number }): Promise<unknown>;

تستدعي دالة المضيف name مع args (أي قيمة JSON؛ وnull بدونها) وتُنجَز بما تُرجعه. host.call لا ترمي استثناءً أبدًا: كل فشل يرفض الوعد (promise)، بـ Error يبيّن code فيه ما حدث (الرسالة للبشر وقد تتغير):

codeالحالة
ERR_NSH_HOST_ERRORفشلت دالة المضيف، أو لا توجد في المضيف دالة name؛ والرسالة هي رسالة المضيف
ERR_ACCESS_DENIEDname ليس ضمن صلاحية host الخاصة بالتطبيق؛ permission هو "host"، وresource الاسم
ERR_FEATURE_UNAVAILABLE_ON_PLATFORMلا مضيف في المنصة: host.available() هو false
ETIMEDOUTلم يجب المضيف في الوقت المحدد
ERR_NSH_QUOTA_EXCEEDED1024 استدعاءً تنتظر بالفعل إجابات التطبيق المضمِّن

الاسم غير الصالح (من 1 إلى 128 من A–Z، a–z، 0–9، _، .، :، -، بدون ..) يُرفض بـ TypeError.

ينتظر الاستدعاء إجابة المضيف 30 ثانية، ثم يُرفض بـ ETIMEDOUT؛ وتُسقَط الإجابة التي تصل بعد ذلك. options.timeout يحدد مدة الانتظار بالمللي ثانية، و0 لعدم وجود حد؛ والمهلة الكبيرة جدًا بحيث لا تبلغها الساعة (نحو 1e22 مللي ثانية) تنتظر بلا حد أيضًا، أما المهلة السالبة أو NaN أو اللانهائية فترفض الاستدعاء بـ RangeError. الوقت هو وقت التطبيق، مثل مؤقِّتاته: في nsh test مع ساعة يدوية يتحرك مع الساعة.

const user = (await host.call("getUserInfo")) as { name: string };
const quote = await host.call("quote.refresh", { symbol: "AAPL" }, { timeout: 5000 }).catch((error) => {
  if (error.code === "ETIMEDOUT" || error.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM") return null;
  throw error;
});

#host.on

on(event: string, listener: (payload: unknown) => void): () => void;

تستدعي أحداث المنصة listener بحمولة كل event يرسله المضيف، إلى أن تُستدعى الدالة التي يُرجعها host.on؛ والمستمع (listener) المضاف أثناء رسم مكوّن يتوقف مع المكوّن. الأحداث التي يرسلها المضيف قبل بدء التطبيق تصل بمجرد بدئه، فالمستمعون المسجَّلون في المستوى الأعلى من التطبيق يستلمونها؛ أما الحدث المرسَل قبل تسجيل المستمع فلا يُعاد إليه. اقرأ الحالة التي يبدأ بها التطبيق عبر host.call، وتابع تغيّراتها عبر host.on.

const stop = host.on("loginChanged", (payload) => console.log(payload));
// later
stop();

#الصلاحية

يجب إدراج كل دالة يستدعيها التطبيق في capabilities.host الخاص بـ nsh.json: أسماء دقيقة أو بادئات تنتهي بـ *، أو true لها جميعًا. ينطبق هذا على كل تطبيق، حتى التطبيق الذي لا يملك capabilities أخرى: دوال المضيف لا تُمنح افتراضيًا أبدًا. الاستدعاء غير الممنوح يُرفض بالخطأ ERR_ACCESS_DENIED ويُسجَّل، وhost.post لاسم كهذا إلى تطبيق مضيف يرمي الخطأ نفسه، وبناء تطبيق يستورد host من nsh دون capabilities.host يُصدر تحذيرًا. الأحداث لا تحتاج إلى منح. راجع الحماية المعزولة والصلاحيات.

{ "capabilities": { "host": ["getUserInfo", "bridge.*"] } }

#المضيف على الويب

على الويب، الصفحة هي المضيف. الخيار host في createPlayer يجيب عن الاستدعاءات، وplayer.hostEvent(event, payload) يرسل الأحداث:

const player = await createPlayer(container, {
  wasm: "./nsh.wasm",
  host: { call: (name, args) => (name === "getUserInfo" ? { name: "Ada" } : Promise.reject(new Error(name))) },
});
player.hostEvent("loginChanged", { signedIn: true });

يحدد التطبيق المنشور مضيفه في nsh.json: web.host وحدة JS في مجلد التطبيق، نسبةً إلى nsh.json، تُصدِّر createHost(player) التي تعيد الكائن الذي يحتوي call. nsh package --target web يشحنها مع الموقع، وnsh run --target web يشغّلها في صفحة التطوير، وnsh test يشغّلها للتطبيقات التي تفتحها اختباراته. web.connect يسرد المضيفين الذين تصل إليهم طلبات المضيف نفسه، والتي تسمح بها Content-Security-Policy الخاصة بالصفحة إلى جانب capabilities.network الخاص بالتطبيق. راجع الويب.

{ "web": { "host": "./host.js", "connect": ["api.example.com"] } }

#host.params

المعاملات التي ضبطها التطبيق المضيف قبل أن يبدأ تطبيقك، وتُقرأ بشكل متزامن من أول سطر في شيفرتك؛ {} عندما لم يضبط أيًا منها. تبقى كما هي أثناء تشغيل تطبيقك: الطلبات اللاحقة تأتي كأحداث.

const conversation = host.params.conversation as string | undefined;
mount(() => (conversation ? <Conversation id={conversation} /> : <Inbox />));

#قناة Native SDK

يحتفظ send في Native SDK بالأحداث إلى أن يستمع التطبيق، حتى 256 حدثًا. عندما يتوقف آخر مستمع أثناء التسليم، تبقى الأحداث المتبقية في الانتظار بالترتيب؛ والأحداث المرسلة أثناء الإغلاق تنتظر التطبيق التالي الذي يُحمَّل. host.post(name, data) يسلّم البيانات إلى معالج Native SDK، الذي يميّز isPost بين الإرسالات (posts) والاستدعاءات (calls).

#جانب التطبيق المضيف

يحتفظ كل عرض بقناة واحدة لكل تطبيق يحمّله. اضبط المعاملات قبل التحميل، وأرسل الأحداث في أي وقت من أي خيط، وسجّل المعالجات بالاسم. تتطلب الاستدعاءات والإرسالات منحة capabilities.host للتطبيق. يجيب المعالج مرة واحدة، الآن أو لاحقًا؛ والإجابة التي لا تصل أبدًا تُفشل الاستدعاء.

// Any platform's AppView (GPUI, macOS, Windows, Linux, iOS): its HostChannel.
use serde_json::json;

let host = view.host();
let mut params = serde_json::Map::new();
params.insert("conversation".into(), json!("42"));
host.set_params(params);
// Runs on the engine thread: answer at once, or move the call and answer later from any thread.
host.handle("user.profile", |call| {
    let id = call.data()["id"].clone();
    call.resolve(json!({ "id": id, "name": "Ada" }));
});
host.send("conversation.open", json!({ "id": "43" })).unwrap();

راجع التضمين على iOS والتضمين على Android وGPUI.

#keymap

يربط keymap اختصارات لوحة المفاتيح في التطبيق: ضغطة مفتاح مثل "mod+k" والدالة التي تشغّلها. تستدعي شيفرة Rust كلًّا من on_key وon_key_with وkey_label من nsh_core::prelude؛ وتستورد TSX keymap من nsh. كلاهما يقبل ضغطات المفاتيح نفسها ويتبع القواعد نفسها.

use nsh_core::prelude::*; // on_key, on_key_with, KeyOptions, key_label
const keymap: {
  bind(keystroke: string, handler: (e: { keystroke: string }) => void, options?: KeyBindingOptions): () => void;
  bind(bindings: Record<string, (e: { keystroke: string }) => void>, options?: KeyBindingOptions): () => void;
  label(keystroke: string): string;
};

interface KeyBindingOptions {
  when?: "always" | "notEditing";
  repeat?: boolean;
}
TSRust
keymap.bind(keystroke, handler)on_key(keystroke, handler)
keymap.bind(keystroke, handler, { when, repeat })on_key_with(keystroke, KeyOptions { when, repeat }, handler)؛ when هو nsh_core::keymap::When (Auto، Always، NotEditing)
keymap.bind({ … })on_key واحد لكل ضغطة مفتاح
keymap.label(keystroke)key_label(keystroke)

يعيش ربط Rust ما دام المكوّن (المالك) الذي أنشأه حيًّا؛ ويتلقى المعالج ضغطة المفتاح كما كُتبت.

تعمل الاختصارات على macOS وWindows وLinux وفي المتصفحات (مفاتيح المتصفح نفسه، مثل Ctrl+T أو Cmd+W، لا تصل إلى الصفحة أبدًا)، وعلى iOS وAndroid مع لوحة مفاتيح فعلية: لوحة مفاتيح iPad، أو Mac يشغّل تطبيق iPad، أو جهاز لوحي أو Chromebook، أو المحاكيين. تعمل الشيفرة نفسها في كل مكان؛ وبلا لوحة مفاتيح لا يُطلق أي مفتاح هذه الارتباطات.

#keymap.bind

bind(keystroke: string, handler: (e: { keystroke: string }) => void, options?: KeyBindingOptions): () => void;

يشغّل handler عند الضغط على keystroke، ويعيد دالة تزيل الربط. يتلقى المعالج { keystroke }: ضغطة المفتاح كما كتبتها، بحالة أحرفها. في Rust: on_key(keystroke, handler)، ويتلقى معالجه ضغطة المفتاح نفسها.

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    let open = signal(false);
    on_key("mod+k", move |_| open.set(true));
    on_key("escape", move |_| open.set(false));
    text(move || if open.get() { "Search…" } else { "Press ⌘K" })
}

مرّر كائنًا لربط عدة ضغطات مفاتيح دفعة واحدة؛ والدالة المعادة تزيلها جميعًا. في Rust، كل ضغطة مفتاح هي on_key واحد:

on_key("mod+s", move |_| save());
on_key("mod+shift+p", move |_| open_palette());
on_key("f5", move |_| refresh());

الربط الذي يُنشأ أثناء رسم مكوّن يُزال مع المكوّن، فتعيش اختصارات الشاشة ما دامت الشاشة حيّة:

#[component]
fn Editor() -> impl IntoElement {
    on_key("mod+enter", move |_| send());
    input().multiline(true)
}

مع خادم التطوير، يؤدي تعديل وحدة إلى إزالة الارتباطات التي أنشأتها شيفرتها العليا قبل تشغيلها من جديد، فلا تترك إعادة التحميل الفورية اختصارات قديمة خلفها.

#keymap.label

label(keystroke: string): string;

يعيد keystroke كما تعرضه القوائم والتلميحات في المكان الذي يعمل فيه التطبيق: "⇧⌘L" على macOS وiOS (وفي متصفح على Mac أو iPhone أو iPad)، و"Ctrl+Shift+L" على Windows وLinux والمتصفحات الأخرى، للمدخل "mod+shift+l". في Rust: key_label(keystroke). استخدمه لعرض اختصارات التطبيق بالمفاتيح التي تعمل فعلًا:

text(format!("Search {}", key_label("mod+k")))

يرمي TypeError عند ضغطة مفتاح غير صالحة، كما يفعل bind؛ أما key_label وon_key فتُحدثان panic بالرسالة نفسها.

#ضغطات المفاتيح

ضغطة المفتاح هي معدِّلات ومفتاح تجمعها +: "mod+k"، "ctrl+shift+p"، "alt+enter"، "escape"، "f5"، "cmd+-" (Command وعلامة الناقص).

المعدِّلmacOSWindowsLinux
mod (ويُكتب أيضًا cmdorctrl)Command (⌘)CtrlCtrl
cmd (ويُكتب أيضًا command، meta، super، win)Command (⌘)مفتاح WindowsSuper
ctrl (ويُكتب أيضًا control)Control (⌃)CtrlCtrl
alt (ويُكتب أيضًا option، opt)Option (⌥)AltAlt
shiftShift (⇧)ShiftShift

يتبع iOS نظام macOS (mod هو Command) ويتبع Android نظامي Windows وLinux (mod هو Ctrl، وcmd مفتاح Meta). في المتصفح، تتبع المفاتيح المنصة التي يعمل عليها المتصفح: mod هو Command على Mac أو iPhone أو iPad، وCtrl في غيرها.

استخدم mod لاختصارات التطبيق الخاصة به: فهو المفتاح الذي تستخدمه تطبيقات كل منصة لها (⌘K على macOS وCtrl+K على Windows وLinux)، فتناسب صياغة واحدة كل مكان. مفتاح Windows وSuper يخصّان النظام في الغالب، لذا نادرًا ما يكون cmd مناسبًا خارج macOS.

المفتاح حرف أو رقم أو علامة ترقيم، يُكتب كالحرف الذي يطبعه على لوحة مفاتيح أمريكية بدون Shift (k، 1، ,، /، [؛ "shift+/" لـ ?، "shift+k" لـ Shift+K)، أو اسم: plus (مفتاح = / +، ويُكتب أيضًا =)، minus (ويُكتب أيضًا -)، space، enter (return)، escape (esc)، tab، backspace، delete (del)، insert، الأسهم up، down، left، right (arrowup، …)، home، end، pageup، pagedown، f1 إلى f24. حالة الأحرف لا تهم: "Mod+Shift+K" هو "mod+shift+k"، والحرف الكبير هو المفتاح نفسه وليس Shift ("mod+K" هو "mod+k"). الأحرف الصغيرة هي النمط الموصى به، وهو ما تستخدمه هذه الصفحات. "mod-k" و"mod++" خطآن تعطي رسالتاهما الصياغة الصحيحة. تقبل اختصارات القوائم في nsh/desktop ضغطات المفاتيح نفسها.

تعمل الاختصارات مع أي تخطيط للوحة المفاتيح: ففي تخطيط سيريلي أو يوناني، mod+k هو المفتاح الذي يكتب K في التخطيط الأمريكي، كما هي الحال مع اختصارات القوائم.

bind يرمي TypeError يوضح الخلل في ضغطة المفتاح غير الصالحة (keymap.bind: invalid keystroke "cmd+kk": unknown key "kk")، ويرفض ضغطات المفاتيح التي يحتفظ بها النظام لنفسه، فلا يستطيع تطبيق كسرها أبدًا:

المنصةالمحجوز
macOS، iOSCmd+Q (إنهاء)، Cmd+H (إخفاء)، Option+Cmd+H (إخفاء الباقي)، Cmd+M (تصغير)، Ctrl+Cmd+F (ملء الشاشة)
Linux، Windows، AndroidCtrl+Q (إنهاء)، Alt+F4 (إغلاق)، F11 (ملء الشاشة)
الويبلا شيء (المتصفح يحتفظ بمفاتيحه)

#أثناء الكتابة

يحتاج حقل النص الذي يملك تركيز لوحة المفاتيح إلى مفاتيحه: الكتابة، وتحريك المؤشر، والحذف، وتحديد الكل، والقص، والنسخ، واللصق، والتراجع، والإعادة. لذلك لا يعمل الربط افتراضيًا أثناء تركيز حقل نص (أو عرض ويب) إذا كانت ضغطة مفتاحه واحدة من هذه: المفاتيح العادية والمفاتيح مع Shift أو Option فقط، والأسهم، وHome، وEnd، وPage Up / Down، وReturn، وTab، وEscape، وBackspace، وDelete، وmod+a، mod+c، mod+v، mod+x، mod+z. أما ضغطات المفاتيح الأخرى مع Command أو Control، ومفاتيح الوظائف، فتعمل أينما كان التركيز.

when يغيّر ذلك (في Rust: KeyOptions { when: When::Always, .. }):

whenيعمل
(الافتراضي)كما سبق
"always"أيضًا في حقول النص، قبل أن يرى الحقل المفتاح
"notEditing"أبدًا أثناء تركيز حقل نص
use nsh_core::keymap::When;

// Sends from the message field too.
on_key_with("mod+enter", KeyOptions { when: When::Always, ..KeyOptions::default() }, move |_| send());
// Only when the user is not typing.
on_key_with("mod+e", KeyOptions { when: When::NotEditing, ..KeyOptions::default() }, move |_| archive());

على iOS وAndroid، يبقى المفتاح الذي تؤلّف به طريقة الإدخال (اختيار مرشّح، تثبيت) من شأن طريقة الإدخال. وفي واجهة GPUI الخلفية، يُعدّ المكوّن الأصلي الذي يملك تركيز لوحة المفاتيح حقل نص، لأنه قد يكون محرّر نصوص. وهو يرى كل مفتاح أولًا: المفتاح الذي يعالجه بنفسه لا يصل إلى الارتباطات أبدًا، والارتباطات التي تعمل أثناء تحرير النص تتلقى المفاتيح التي يتركها له.

#أي ربط يُنفَّذ

المفتاح الذي يُطلق ربطًا يُستهلك: لا يُكتب، ولا يُصدر النظام صوت تنبيه، ولا ترى مفاتيحُ NSH نفسها هذا المفتاح (Escape للرجوع، وTab لنقل التركيز، والأسهم للتمرير). عندما تتشارك عدة ارتباطات ضغطة المفتاح نفسها، يعمل أحدثها مما يجوز له العمل، فيستطيع حوار ربط Escape فوق ربط الشاشة، ويعود ربط الشاشة عندما يزول ربط الحوار. تُفحص الارتباطات أصليًا لحظة الضغط على المفتاح؛ ويعمل المعالج بعد ذلك مباشرة، على خيط المحرك (Rust) أو خيط JS (TSX).

الضغط المطوّل على مفتاح يُطلق ربطه مرة واحدة، حتى يُترك المفتاح؛ ومع repeat: true يُطلقه من جديد عند كل تكرار تلقائي (للتنقل عبر قائمة مثلًا):

on_key_with("alt+down", KeyOptions { repeat: true, ..KeyOptions::default() }, move |_| next_item());

تفسح اختصارات القوائم المجال لربط بضغطة المفتاح نفسها: قائمة التطبيق (nsh/desktop) على كل أنظمة سطح المكتب، وعلى macOS القوائم المدمجة أيضًا (Cmd+W وCmd+C …). أثناء التطوير باستخدام nsh run، يعيد Cmd+R / Ctrl+R تحميل التطبيق دائمًا، حتى لو ربط mod+r؛ أما التطبيق المعبّأ فيستطيع ربطه.

#nativeComponent و nativeModule

nativeComponent وnativeModule يصلان تطبيقك بما يوفره التطبيق المضيف، بالاسم، وقت التشغيل. يستطيع تطبيق iOS أو Android يضمّن NSH أن يسجّل عروضه الخاصة (خريطة، أو مخطط، أو معاينة كاميرا) بالاسم؛ وnativeComponent يعطيك مكوّنًا مضبوط الأنواع لأحدها. ولأن NSH يرسم بعروض أصلية حقيقية، يقع هذا المكوّن في تسلسل العروض مثل أي عرض آخر: يتولى NSH تخطيطه، ويُقتطع ويُمرَّر مع آبائه، ويتراكب بشكل صحيح. ويستطيع التطبيق الذي يضمّن NSH من Rust أيضًا تسجيل دوال وفئات ومكوّنات Rust؛ وnativeModule يعطيك إياها ككائن واحد مضبوط الأنواع. يُبنى تطبيقك بـ nsh build الاعتيادي في الحالتين: يربط المضيف ما يوفره عندما يشغّل التطبيق. تضع شيفرة Rust عرضًا مسجّلًا بـ native(name) من nsh_core::prelude، وتضبط خصائصه بـ .prop(name, value) (قيمة JSON، أو signal أو closure لها)، أو تربط إحداها بقيمة متحركة بـ .bind_prop(name, &value)، وتسمع أحداثه بـ .on_native_event(…)، وتستدعي أوامره عبر ElementRef؛ وتستورد TSX nsh.

use nsh_core::prelude::*; // native, NativeEvent, element_ref

راجع المكوّنات الأصلية لتسجيل الجانب الأصلي، والتضمين على iOS / التضمين على Android.

#nativeComponent

function nativeComponent<P extends object = {}, E extends object = {}>(
  name: string,
  spec: NativeComponentSpec<P, E>,
): (props: NativeComponentProps<P, E>) => JSX.Element;

interface NativeComponentSpec<P, E> {
  /** Its own props: sent to the component (any JSON value), each when it changes. */
  props: readonly (keyof P & string)[];
  /** The events it emits. */
  events?: readonly (keyof E & string)[];
}

يعيد مكوّنًا للمكوّن الأصلي الذي سجّله التطبيق المضيف باسم name (في Swift NshAppView.register(component:)، وفي Java NshView.registerComponent). في Rust: native(name)، وهو عرض خصائصه قيم JSON (.prop("text", json!(…))) وتصل أحداثه على هيئة NativeEvent { name, data }.

  • P هو نوع خصائصه الخاصة. كل خاصية مدرجة في spec.props تُرسل إلى الجانب الأصلي بصيغة JSON عندما تتغير؛ وضبطها على undefined يزيلها.
  • E يربط أسماء الأحداث بحمولاتها. الحدث regionChange يصبح خاصية المعالج onRegionChange.
  • يقبل المكوّن أيضًا كل خاصية عرض عدا children: style وclass والتخطيط وإمكانية الوصول ومعالجات اللمس وref.
type NativeComponentProps<P, E> = Omit<ViewProps, "children"> & P & NativeEventHandlers<E>;
type NativeEventHandlers<E> = { [K in keyof E & string as `on${Capitalize<K>}`]?: (data: E[K]) => void };

#الحجم

يُخطَّط المكوّن كما يُخطَّط العرض، بالحجم الذي يعطيه له نمطه. وحيثما يترك النمط بُعدًا مفتوحًا، يستخدم الحجم الذي يبلغ عنه المكوّن الأصلي (كما تستخدم الصورة حجمها الطبيعي).

#الأوامر

يتلقى ref الخاص به NativeComponentRef؛ وref.command(name, args) يستدعي الأمر name في المكوّن الأصلي مع args (أي قيمة JSON). في Rust: ElementRef::command(name, args).

map.command("zoomTo", json!({ "level": 3 }));

#عند عدم تسجيل شيء

حيثما لا يوجد شيء مسجّل تحت name (تطبيق مضيف مختلف، أو مضيف التطوير، أو المتصفح)، يرسم المكوّن عرضًا فارغًا ويُسجَّل تحذير مرة واحدة. أبقِ التطبيق قابلًا للاستخدام بدونه، أو تحقق من مضيفك قبل استخدامه.

#مثال

الشارة هنا عرض أصلي في التطبيق المضيف، مسجّل باسم embed.badge. يتولى NSH تخطيطها، ويرسل إليها text وcolor، ويتلقى أحداث tap منها، ويستدعي أمرها pulse:

use nsh_core::prelude::*;
use serde_json::json;

#[component]
pub fn App() -> impl IntoElement {
    let taps = signal(0_u64);
    let badge = element_ref();
    view()
        .flex_1()
        .p_5()
        .gap_4()
        .child(
            native("embed.badge")
                .prop("text", json!("Tap the native badge"))
                .prop("color", json!("#8250df"))
                .element_ref(&badge)
                .on_native_event(move |e: &NativeEvent| {
                    if &*e.name == "tap" {
                        taps.set(e.data["count"].as_u64().unwrap_or(0));
                        badge.command("pulse", json!(null));
                    }
                })
                .self_start()
                .rounded(px(8.0))
                .overflow_hidden(),
        )
        .child(text(move || format!("Tapped {}×", taps.get())))
}

#nativeModule

function nativeModule<T extends object>(specifier: string): T;

تعيد exports المجموعة التي سجّلها التطبيق المضيف باسم specifier: وحداتها وفئاتها ودوالها ومكوّنات Rust، ككائن واحد. يكتب التطبيق النوع T بنفسه، فلا يلزم أي شيء عن المضيف عند بنائه. شيفرة Rust المبنية مع crate المضيف تستدعي هذه الدوال كما هي في Rust؛ أما nativeModule فهو الطريقة التي تصل بها TSX إليها.

import { nativeModule, signal } from "nsh";

interface TradingApi {
  quote(symbol: string): number;
  placeOrder(symbol: string, quantity: number, side: "Buy" | "Sell"): Promise<Order>;
  on(event: "change", listener: (desk: Desk) => void): () => void;
}

const { Trading } = nativeModule<{ Trading: TradingApi }>("host/trading");

const price = Trading.quote("AAPL");

const desk = signal<Desk | null>(null);
Trading.on("change", (d) => desk.set(d));
  • تعمل الاستدعاءات كما تعمل عبر استيراد مجموعة: الدالة العادية في Rust تعيد قيمتها فورًا، والدالة async تعيد promise، والدالة التي تمرّرها تصبح callback يمكن لـ Rust استدعاؤها لاحقًا. يتحقق Rust من كل وسيط، ويرمي TypeError عند وجود وسيط خاطئ.
  • كل استدعاء بالقيمة specifier نفسها يعيد الكائن نفسه.
  • إذا لم يكن هناك شيء مسجّل تحت specifier، فإن nativeModule ترمي ReferenceError تذكر فيه الاسم. استدعها حيث يستطيع التطبيق التعامل مع ذلك، أو فقط في المضيف الذي صُنعت له.

يسجّل المضيف المجموعة من Rust، باستخدام set_ui_sets الخاصة بـ crate المنصّة (بجانب مجموعات NSH نفسها) قبل أن يبدأ عروضه، أو EngineConfig::ui_sets:

use std::{cell::RefCell, rc::Rc};

use nsh_core::prelude::*;

pub struct Trading;

#[module]
impl Trading {
    /// A sample price.
    pub fn quote(symbol: String) -> Result<f64, String> { /* ... */ }

    /// Calls `listener` with the desk now and after every change
    /// (`change`, the one event), until the returned function is called
    /// or what called `on` goes.
    pub fn on(event: String, listener: Callback<Desk>, cx: CallCx<'_>) -> Result<Stop, String> {
        if event != "change" {
            return Err(format!("no Trading event `{event}`"));
        }
        let listener = Rc::new(RefCell::new(Some(listener)));
        let held = listener.clone();
        cx.start(async move {
            let mut desk = DESK.subscribe(); // a tokio watch channel the host keeps
            loop {
                let Some(listener) = held.borrow().clone() else { return };
                listener.post(desk.borrow_and_update().clone());
                if desk.changed().await.is_err() {
                    return;
                }
            }
        });
        Ok(Stop::new(move || drop(listener.take())))
    }
}

let mut trading = Registry::ui_set("host/trading");
trading.module::<Trading>();
nsh_platform_gpui::set_ui_sets(vec![nsh_base::registry(), trading]);

on هي الطريقة التي يُبقي بها المضيف كل التطبيقات متزامنة مع بياناته: المهمة التي تبدأها تنتظر تغييرات المضيف وترسل كل تغيير إلى التطبيق، فيصل أي تغيير أجراه المضيف، أو أجراه أي تطبيق عبر دالة أخرى، إلى كل تطبيق يستمع. أما Stop الذي تعيده فهو الدالة التي تعيدها Trading.on في TSX: تعمل مرة واحدة، عندما يستدعيها التطبيق أو عندما يزول ما استدعى on (مكوّن، أو تشغيل الوحدة تحت hot reload)، وهنا تُسقط المستمع فتنتهي المهمة.

#إمكانية الوصول

تقرأ واجهة إمكانية الوصول (accessibility) إعدادات إمكانية الوصول لدى المستخدم (هل يعمل قارئ الشاشة، وهل طلب حركة أقل) وتتحدث إلى قارئ الشاشة. تستدعي شيفرة Rust كلًا من screen_reader() وreduce_motion() وannounce_for_accessibility() من nsh_core::prelude وتنقل التركيز باستخدام ElementRef؛ وتستورد TSX كلًا من screenReaderEnabled() وreduceMotion() وannounce() وsetAccessibilityFocus() من nsh. الإعدادات تفاعلية (reactive): اقرأها في binding أو computed أو effect، فيتحدّث ما يقرؤها عندما يغيّرها المستخدم. أما props إمكانية الوصول الخاصة بالعناصر (accessibilityLabel، role، …) فهي موثّقة ضمن العناصر؛ والدليل هو إمكانية الوصول.

use nsh_core::prelude::*; // screen_reader, reduce_motion, announce_for_accessibility, element_ref

#screenReaderEnabled

const screenReaderEnabled: Accessor<boolean>;

هل يعمل قارئ الشاشة (VoiceOver، TalkBack). تفاعلية: يتحدّث ما يقرؤها عندما يشغّل المستخدم قارئ الشاشة أو يوقفه. في Rust: screen_reader() -> bool، وهي تفاعلية بالطريقة نفسها.

when(move || !screen_reader(), || text("Swipe for the next photo"))
    .otherwise(move || text("Next photo").on_press(next))

#reduceMotion

const reduceMotion: Accessor<boolean>;

هل طلب المستخدم حركة أقل (Reduce Motion في iOS، وRemove animations في Android). تفاعلية. في Rust: reduce_motion() -> bool. وفئات Tailwind تتبعها أيضًا عبر المتغيّر motion-reduce:.

let open = move || {
    if reduce_motion() {
        panel.set(0.0);
    } else {
        let _ = panel.spring(0.0, Spring::default());
    }
};

#announce

function announce(message: string): void;

يقرأ قارئ الشاشة message (إعلان في VoiceOver، وفي TalkBack). استخدمها للنتائج التي تظهر دون أن ينتقل التركيز، مثل “Saved”. في Rust: announce_for_accessibility(message).

#setAccessibilityFocus

function setAccessibilityFocus(node: ViewRef): void;

ينقل تركيز قارئ الشاشة إلى node (ref الخاص بعنصر). في Rust: ElementRef::set_accessibility_focus().

use nsh_core::prelude::*;

#[component]
fn Saved() -> impl IntoElement {
    let note = element_ref();
    let save = move || {
        announce_for_accessibility("Saved");
        note.set_accessibility_focus();
    };
    // Runs again when the setting changes, until the component goes.
    effect(|| log::info!("screen reader {}", screen_reader()));
    view()
        .gap_2()
        .child(text("Save").role(AccessibilityRole::Button).on_press(save))
        .child(text("Your changes are kept on this device.").element_ref(&note))
}

#الأنواع

يصدّر nsh أيضًا أنواع واجهة reactive وأنواع عناصر المضيف. وهذه أكثرها استخدامًا:

النوعما هو
Accessor<T>قيمة قابلة للقراءة: () => T
Signal<T>Accessor<T> مع set وupdate
SignalOptions<T>خيارات signal وcomputed (equals)
Context<T>ما تعيده context
Component<P>(props: P) => JSX.Element
JSXفضاء أسماء JSX (JSX.Element)
ViewRef، InputRef، ScrollRef، ListRef، WebViewRef، VideoRef، NativeComponentRefما يستقبله ref، ونوع كل عنصر الخاص به (ViewRef والأنواع المبنية عليه): التركيز والتمرير وأوامر أخرى. وViewRef هو أيضًا ما يمثّله العنصر (JSX.Element). راجع دوال ref
ViewProps، TextProps، ImageProps، ScrollProps، CommonProps، TouchPropsprops عناصر المضيف
ViewStyle، TextStyle، InputStyle، ImageStyle، LayoutStyle، StyleProp<T>كائنات الأنماط؛ راجع مرجع الأنماط
ColorValue، DimensionValue، LengthValue، TransformStepالقيم داخل الأنماط
GestureResponderEvent، MouseEvent، MouseButton، ContextMenuEvent، PressEvent، ScrollEvent، PanEvent، TouchPointحمولات الأحداث
Point، Size، Bounds، Edges، Modifiersالأشكال التي تتكوّن منها الحمولات والـ props، مع حقول GPUI (nsh_core::geometry)
PixelSizeالحجم الخاص بالصورة أو الفيديو بالبكسل (onLoad، Image.getSize، وصور الحافظة والمنتقي)، حيث Size بالنقاط
ImageAssetما ينتج عن استيراد ملف صورة ({ url, size: Size, scales? }؛ وتسرد scales متغيّرات كثافة البكسل الخاصة بها)
OS"ios" | "android" | "macos" | "windows" | "linux" | "web"
AccessibilityRole، AccessibilityStateprops إمكانية الوصول
import { type GestureResponderEvent, type InputRef, signal } from "nsh";

let field: InputRef | undefined;
const onPress = (e: GestureResponderEvent) => field?.focus();

تقبل StyleProp<T> نمطًا واحدًا أو قائمة تُدمج من اليسار إلى اليمين، مع تخطي القيم الخاطئة (falsy):

<View style={[styles.card, selected() && styles.selected]} />