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

التوثيق

المكونات الأصلية (Native Components)

المكوّن الأصلي هو عرض (view) يسجّله التطبيق المضيف باسم معيّن: خريطة، أو معاينة كاميرا، أو مكتبة رسوم بيانية، أو أي UIView على iOS، أو android.view.View على Android، أو NSView على macOS، أو gtk::Widget على Linux، أو رسم Direct2D أو HWND فرعي على Windows. يستخدمه تطبيق NSH مثل أي عنصر آخر: يُنشئه تطبيق Rust باستخدام native(name) من nsh_core::prelude، ويُصرّح به تطبيق TSX باستخدام nativeComponent() من nsh. يتولى NSH ترتيبه مع بقية التطبيق، ويرسل إليه الخصائص (props)، ويسلّم أحداثه إلى المعالجات، ويمرّر إليه الأوامر من ref.

ولأن NSH يرسم باستخدام عروض النظام نفسه، فإن المكوّن الأصلي ليس سوى عرض فرعي آخر في التسلسل الهرمي نفسه: لا توجد مشكلة في الطبقات، فهو يُقصّ ويتمرّر ويستقبل اللمسات مثل <View>.

#كيف تتكامل الأجزاء

  1. يسجّل التطبيق المضيف مصنعًا (factory) تحت اسم معيّن، مثل "embed.badge"، على NshAppView (iOS) أو NshView (Android). لكل عرض سجلّه الخاص؛ ولا يوجد شيء عام.
  2. يصرّح تطبيق NSH بمكوّن لذلك الاسم مع الخصائص والأحداث التي يستخدمها.
  3. عند إنشاء العنصر، ينشئ NSH عرضًا حاويًا (يُزخرف ويُؤطَّر ويخضع لاختبار اللمس مثل أي عرض)، ثم يطلب من المصنع مكوّنًا ويضع عرض المكوّن داخل الحاوية بحيث يملؤها.
  4. يتلقى المكوّن كل خاصية فور إنشائه، ثم يتلقى كل خاصية مرة أخرى عندما تتغير.
  5. يُصدر المكوّن الأحداث وقد يُبلغ عن حجمه الخاص؛ ويستدعي التطبيق أوامره عبر ref.

#استخدام المكوّن في التطبيق

في Rust، ينشئ native(name) العنصر: يمنحه .prop(name, value) خاصية (serde_json::Value، أو إشارة (signal) أو دالة تُرجع واحدة منها، وتُرسل مرة أخرى عند تغيرها)، ويستمع .on_native_event(..) إلى أحداثه على هيئة NativeEvent { name, data }، وتُرسل دوال element_ref() المضافة إليه عبر .element_ref(&r) أوامره. في TSX، يصرّح nativeComponent() بمكوّن للاسم مع الخصائص والأحداث التي يستخدمها.

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

const COLORS: [&str; 4] = ["#8250df", "#0969da", "#1a7f37", "#cf222e"];

#[component]
pub fn App() -> impl IntoElement {
    let taps = signal(0_u64);
    let badge = element_ref();
    view()
        .flex_1()
        .p(px(20.0))
        .pt_safe_area_plus(px(20.0))
        .pb_safe_area_plus(px(20.0))
        .gap(px(16.0))
        .bg(rgb(0xf6f8fa))
        .child(text("Rendered by nsh").text_size(px(22.0)).font_weight(FontWeight::BOLD))
        .child(
            // The host app's badge: a label it draws, sized to its text.
            native("embed.badge")
                .element_ref(&badge)
                .prop("text", move || match taps.get() {
                    0 => json!("Tap the native badge"),
                    n => json!(format!("Tapped {n}×")),
                })
                .prop("color", move || json!(COLORS[taps.get() as usize % COLORS.len()]))
                .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!("Rust saw {} taps", taps.get())))
}

نسخة TSX هي examples/embedded/App.tsx، وهي التي يشغّلها نموذجا التضمين في المستودع.

#nativeComponent(name, spec)

function nativeComponent<P extends object = {}, E extends object = {}>(
  name: string,
  spec: NativeComponentSpec<P, E>,
): (props: NativeComponentProps<P, E>) => JSX.Element;
المعاملالنوعالوصف
namestringالاسم الذي سجّله التطبيق المضيف. استخدم بادئة خاصة بك ("maps.view"، "acme.chart") لتبقى الأسماء فريدة.
spec.propsreadonly (keyof P & string)[]الخصائص الخاصة بالمكوّن. لا تُرسل إلى الجانب الأصلي إلا الخصائص المدرجة هنا.
spec.eventsreadonly (keyof E & string)[]الأحداث التي يُصدرها. اختياري.

تصف معاملات الأنواع الخصائص (P) وحمولة كل حدث (E). يقبل المكوّن المُرجَع ما يلي:

  • الخصائص الخاصة بالمكوّن P؛
  • معالجًا لكل حدث: يعالج onTap الحدث tap، ويعالج onRegionChange الحدث regionChange (يُكتب الحرف الأول كبيرًا وتُضاف on في البداية)؛
  • كل ما يقبله <View> باستثناء العناصر الفرعية: style، وclass / classList (Tailwind)، والتخطيط، وخصائص إمكانية الوصول، وtestID، وonLayout، وref. وهذه تخص الحاوية التي يرسمها NSH، لا المكوّن.

المكوّن الأصلي عنصر طرفي (leaf): لا يقبل عناصر فرعية.

#الخصائص

تُرسل كل خاصية خاصة بالمكوّن بصيغة JSON، لذا يمكن أن تكون قيمتها أي قيمة JSON: سلاسل نصية، وأرقامًا، وقيمًا منطقية، وnull، ومصفوفات، وكائنات بسيطة. والخصائص تفاعلية مثل غيرها: عندما تقرأ دالة خاصية في Rust (.prop("text", move || json!(label.get()))) أو text={label()} في TSX إشارة، تُرسل القيمة الجديدة عند تغير الإشارة، وفي ذلك الحين فقط.

ما يحدث في التطبيقما يتلقاه المكوّن
إنشاء العنصركل خاصية خاصة به مضبوطة، فور إنشاء المكوّن
تغير قيمة خاصيةتلك الخاصية بقيمتها الجديدة
تصبح الخاصية undefinedتلك الخاصية دون قيمة (nil في Swift، null في Java)
تتحرك خاصية مرتبطة بقيمة متحركةsetAnimatedNumber / setAnimatedColor، في الإطارات التي تتغير فيها

تحمل الخصائص قيم JSON؛ فاجعل الواجهة مقتصرة على البيانات (تبقى الدوال ونُسخ الأصناف في التطبيق).

#الخصائص المتحركة

يمكن أن تكون الخاصية أيضًا قيمة متحركة: AnimatedValue، أو عقدة مشتقة منها (interpolate، أو scrollValue أو pullValue الخاصة بعرض التمرير). ولا تُرسل حينئذ بصيغة JSON: يربطها NSH، ويُسلَّم المكوّن القيمة على خيط واجهة المستخدم في كل إطار تتغير فيه، دون المرور عبر JS، فيستطيع تتبّع سحب أو سحبًا للتحديث.

const pull = createAnimatedValue(0);
<Scroll pullValue={pull} refreshIndicator={{ height: 72 }} refreshing={refreshing()} onRefresh={refresh}>
  <Spinner progress={pull.interpolate({ inputRange: [0, 72], outputRange: [0, 1] })} />
</Scroll>

يتلقى المكوّن الأرقام عبر setAnimatedNumber(name, value) والألوان عبر setAnimatedColor(name, argb) (0xAARRGGBB)؛ وبشكل افتراضي يستدعي كلاهما setProp مع الرقم، لذا فإن المكوّن الذي لا يطبّقهما يظل يحصل على القيمة. تطبّق مكوّنات Rust set_animated_number وset_animated_color، ويربط كود Rust الخاصية باستخدام native(name).bind_animated_prop("progress", &value). وإعادة منح الخاصية قيمة عادية تفك الربط وترسل تلك القيمة. ولا توجد مكوّنات مسجّلة على الويب.

#الأحداث

يُصدر المكوّن حدثًا باسمه مع حمولة JSON اختيارية. يسلّمه NSH إلى خيط المحرك ويستدعي المعالج المطابق مع الحمولة (في Rust، المستمع الوحيد on_native_event مع name وdata الخاصين بالحدث):

use serde_json::json;

native("acme.map")
    .prop("latitude", json!(52.52))
    .prop("longitude", json!(13.405))
    .on_native_event(move |e: &NativeEvent| match &*e.name {
        "regionChange" => center.set((
            e.data["latitude"].as_f64().unwrap_or_default(),
            e.data["longitude"].as_f64().unwrap_or_default(),
        )),
        "markerPress" => open(e.data["id"].as_str().unwrap_or_default()),
        _ => {}
    })
    .flex_1()

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

#الأوامر

تمر الاستدعاءات الأمرية (التكبير إلى نقطة، وبدء التشغيل، ونبض الرسوم المتحركة) عبر المرجع (ref) الخاص بالعنصر (element_ref() في Rust، ref في TSX):

let map = element_ref();

native("acme.map")
    .element_ref(&map)
    .prop("latitude", json!(52.52))
    .prop("longitude", json!(13.405))
    .flex_1()

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

يرسل command(name, args) (في TSX: command(name, args?)) الاسم والوسائط (أي قيمة JSON) إلى دالة command في المكوّن. تُطبَّق الأوامر بالترتيب مع التغييرات التي سبقتها، لذا فإن الأمر الصادر مباشرة بعد تغيير خاصية يرى الخاصية الجديدة.

#تحديد الحجم

تُرتَّب الحاوية مثل <View>:

  • حيثما يحدد النمط بُعدًا (width، height، flex: 1، alignSelf: "stretch"، …)، يكون لذلك البعد الأولوية.
  • حيثما يترك النمط بُعدًا مفتوحًا، تأخذ الحاوية الحجم الذي أبلغ عنه المكوّن عبر setSize، مثلما تأخذ الصورة حجمها الطبيعي. والمكوّن الذي لا يبلغ عن حجم أبدًا يُقاس بصفر على الأبعاد المفتوحة.

أبلغ عن الحجم بالنقاط (points) في iOS أو dp في Android كلما تغير المحتوى. تقيس الشارة أعلاه تسميتها في كل مرة يتغير فيها text وتبلغ عن حجم التسمية مضافًا إليه الحشو، لذا يجعل alignSelf: "flex-start" الشارة بعرض نصها تمامًا.

#التسجيل على iOS (Swift)

طبّق NshComponent وسجّل مصنعًا على NshAppView:

import NativeShell
import UIKit

/// `embed.badge`: props `text` and `color`; emits `tap`; command `pulse`.
final class Badge: NshComponent {
    let view = UIView()
    private let label = UILabel()
    private let context: NshComponentContext
    private var taps = 0

    init(_ context: NshComponentContext) {
        self.context = context
        label.textColor = .white
        label.font = .boldSystemFont(ofSize: 17)
        label.textAlignment = .center
        label.frame = view.bounds
        label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(label)
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
    }

    func setProp(_ name: String, _ value: Any?) {
        switch name {
        case "text":
            label.text = value as? String
            let size = label.intrinsicContentSize
            context.setSize(CGSize(width: size.width + 32, height: size.height + 20))
        case "color":
            view.backgroundColor = (value as? String).flatMap(UIColor.init(hex:)) ?? .systemPurple
        default:
            break
        }
    }

    func command(_ name: String, _ args: Any?) {
        guard name == "pulse" else { return }
        view.alpha = 0.3
        UIView.animate(withDuration: 0.6) { self.view.alpha = 1 }
    }

    @objc private func tapped() {
        taps += 1
        context.emit("tap", ["count": taps])
    }
}

// Before loading the bundle:
app.register(component: "embed.badge") { context in Badge(context) }

(UIColor(hex:) دالة مساعدة صغيرة في النموذج، وليست جزءًا من NativeShell.)

APIالوصف
NshAppView.register(component:_:)يسجّل مصنعًا (NshComponentContext) -> NshComponent تحت اسم معيّن. وينشئ مكوّنًا واحدًا لكل عنصر يُنشأ بعد ذلك. تسجيل اسم مرة أخرى يستبدل المصنع.
protocol NshComponentvar view: UIView (يملأ الحاوية)، setProp(_:_:)، command(_:_:) (اختياري: الافتراضي يتجاهل الأوامر). معزول على الخيط الرئيسي (main actor).
setProp(_ name: String, _ value: Any?)القيم كائنات JSON من Foundation: String، NSNumber، [Any]، [String: Any]، NSNull؛ وnil عند إزالة الخاصية.
NshComponentContext.emit(_:_:)يُصدر حدثًا؛ ويجب أن تكون البيانات شيئًا يقبله JSONSerialization (أو nil). آمن للاستخدام عبر الخيوط.
NshComponentContext.setSize(_:)يُبلغ عن حجم المكوّن الخاص بالنقاط. آمن للاستخدام عبر الخيوط.

#التسجيل على Android (Java / Kotlin)

طبّق com.longbridge.nsh.NshComponent وسجّل مصنعًا على NshView:

import com.longbridge.nsh.NshComponent;
import com.longbridge.nsh.NshComponentContext;

final class Badge implements NshComponent {
    private final TextView label;
    private final NshComponentContext context;
    private final float density;
    private int taps;

    Badge(Context app, NshComponentContext context) {
        this.context = context;
        density = app.getResources().getDisplayMetrics().density;
        label = new TextView(app);
        label.setTextColor(Color.WHITE);
        label.setTextSize(17);
        label.setGravity(Gravity.CENTER);
        label.setOnClickListener(v -> context.emit("tap", Collections.singletonMap("count", ++taps)));
    }

    @Override
    public View view() {
        return label;
    }

    @Override
    public void setProp(String name, Object value) {
        if (name.equals("text")) {
            label.setText(value instanceof String ? (String) value : "");
            label.measure(0, 0);
            // Sizes are in dp.
            context.setSize(label.getMeasuredWidth() / density + 32, label.getMeasuredHeight() / density + 12);
        } else if (name.equals("color")) {
            label.setBackgroundColor(value instanceof String ? Color.parseColor((String) value) : 0xff8250df);
        }
    }

    @Override
    public void command(String name, Object args) {
        if (!name.equals("pulse")) return;
        label.setAlpha(0.3f);
        label.animate().alpha(1).setDuration(600).start();
    }
}

// Before loading the bundle:
nsh.registerComponent("embed.badge", context -> new Badge(this, context));

والكود نفسه في Kotlin:

nsh.registerComponent("embed.badge") { context -> Badge(this, context) }
APIالوصف
NshView.registerComponent(String name, NshComponent.Factory factory)يسجّل مصنعًا تحت اسم معيّن؛ وينشئ مكوّنًا واحدًا لكل عنصر يُنشأ بعد ذلك (على خيط واجهة المستخدم).
interface NshComponentView view()، void setProp(String, Object)، default void command(String, Object)، default void destroy() (العنصر زال: أُزيل، أو أوقفه NshView.destroy() تطبيقه، أو استُبدل بتحميل آخر؛ يُستدعى مرة واحدة لكل مكوّن). يُستدعى على خيط واجهة المستخدم.
قيم setPropString، Number، Boolean، java.util.List، java.util.Map، أو null عند الإزالة (أو JSON null).
NshComponentContext.emit(String event, Object data)يُصدر حدثًا؛ وdata بأنواع Java نفسها، أو null. من أي خيط.
NshComponentContext.setSize(float width, float height)يُبلغ عن حجم المكوّن الخاص بوحدة dp. من أي خيط.

#التسجيل من Rust

التطبيقات التي تضمّن بيئة تشغيل iOS من Rust (nsh-platform-ios) تسجّل NativeComponent على AppView:

use std::rc::Rc;
use nsh_core::JsonValue;
use nsh_platform_ios::{AppView, ComponentContext, NativeComponent};
use objc2_ui_kit::UIView;

struct Badge {
    view: objc2::rc::Retained<UIView>,
    context: ComponentContext,
}

impl NativeComponent for Badge {
    fn view(&self) -> &UIView {
        &self.view
    }

    fn set_prop(&mut self, name: &str, value: Option<&JsonValue>) {
        // ... update the view, then report its size in points:
        self.context.set_size(120.0, 40.0);
    }

    fn command(&mut self, name: &str, args: &JsonValue) {
        // optional
    }
}

fn register(app: &AppView, make_view: impl Fn() -> objc2::rc::Retained<UIView> + 'static) {
    app.register_component(
        "embed.badge",
        Rc::new(move |context: ComponentContext| -> Box<dyn NativeComponent> {
            Box::new(Badge { view: make_view(), context })
        }),
    );
}

يرسل ComponentContext::emit(name, data) حدثًا (data هو serde_json::Value) ويُبلغ set_size(width, height) عن الحجم؛ وكلاهما يعمل من أي خيط.

#التسجيل على macOS (Rust)

على macOS يكون المكوّن NSView، ويُسجَّل على AppView الخاص بـ nsh-platform-macos:

use std::rc::Rc;
use nsh_core::JsonValue;
use nsh_platform_macos::{
    objc2::{rc::Retained, MainThreadMarker},
    objc2_app_kit::{NSTextField, NSView},
    objc2_foundation::NSString,
    AppView, ComponentContext, NativeComponent,
};

struct Badge {
    label: Retained<NSTextField>,
    context: ComponentContext,
}

impl NativeComponent for Badge {
    fn view(&self) -> Retained<NSView> {
        Retained::into_super(Retained::into_super(self.label.clone()))
    }

    fn set_prop(&mut self, name: &str, value: Option<&JsonValue>) {
        if name == "text" {
            let text = value.and_then(|v| v.as_str()).unwrap_or("");
            self.label.setStringValue(&NSString::from_str(text));
            self.context.set_size(120.0, 40.0);
        }
    }

    fn command(&mut self, name: &str, args: &JsonValue) {
        // optional
    }
}

fn register(app: &AppView, mtm: MainThreadMarker) {
    app.register_component(
        "embed.badge",
        Rc::new(move |context: ComponentContext| -> Box<dyn NativeComponent> {
            Box::new(Badge { label: NSTextField::labelWithString(&NSString::new(), mtm), context })
        }),
    );
}

يملأ العرض الحاوية التي يرتّبها NSH؛ ويعمل ComponentContext::emit وset_size (بالنقاط) من أي خيط، كما في iOS. سجّل قبل AppView::load؛ فالسجلّ يخص AppView واحدًا (والعروض المنشأة من AppOptions نفسه).

#التسجيل على Linux (Rust)

على Linux يكون المكوّن عنصر واجهة GTK، ويُسجَّل على AppView الخاص بـ nsh-platform-linux:

use std::rc::Rc;
use nsh_core::JsonValue;
use nsh_platform_linux::{gtk4 as gtk, AppView, ComponentContext, NativeComponent};
use gtk::prelude::*;

struct Badge {
    label: gtk::Label,
    context: ComponentContext,
}

impl NativeComponent for Badge {
    fn widget(&self) -> gtk::Widget {
        self.label.clone().upcast()
    }

    fn set_prop(&mut self, name: &str, value: Option<&JsonValue>) {
        if name == "text" {
            self.label.set_text(value.and_then(|v| v.as_str()).unwrap_or(""));
            self.context.set_size(120.0, 40.0);
        }
    }

    fn command(&mut self, name: &str, args: &JsonValue) {
        // optional
    }
}

fn register(app: &AppView) {
    app.register_component(
        "embed.badge",
        Rc::new(|context: ComponentContext| -> Box<dyn NativeComponent> {
            Box::new(Badge { label: gtk::Label::new(None), context })
        }),
    );
}

يملأ عنصر الواجهة الحاوية التي يرتّبها NSH؛ ويعمل ComponentContext::emit وset_size (بالنقاط) من أي خيط، كما في iOS. سجّل قبل AppView::load. السجلّ يخص AppView واحدًا (والعروض المنشأة من AppOptions نفسه، الذي يكون components حقلًا فيه).

#التسجيل على Windows (Rust)

على Windows يرسم NSH كل عرض بنفسه باستخدام Direct2D، لذا إما أن يرسم المكوّن داخل حاويته باستخدام Direct2D (draw) أو يأتي بنافذة خاصة به (window: عنصر تحكم خريطة، أو عرض من جهة خارجية)، ويُسجَّل على AppView الخاص بـ nsh-platform-windows:

use std::rc::Rc;
use nsh_core::JsonValue;
use nsh_platform_windows::{
    windows::Win32::Graphics::Direct2D::{
        Common::{D2D1_COLOR_F, D2D_RECT_F},
        ID2D1DeviceContext,
    },
    AppView, ComponentContext, NativeComponent,
};

/// A box filled with the color of its `on` prop.
struct Badge {
    on: bool,
    context: ComponentContext,
}

impl NativeComponent for Badge {
    fn draw(&self, dc: &ID2D1DeviceContext, size: (f32, f32)) {
        let color = if self.on {
            D2D1_COLOR_F { r: 0.2, g: 0.7, b: 0.3, a: 1.0 }
        } else {
            D2D1_COLOR_F { r: 0.6, g: 0.6, b: 0.6, a: 1.0 }
        };
        // SAFETY: Direct2D calls on the context nsh draws with.
        unsafe {
            if let Ok(brush) = dc.CreateSolidColorBrush(&color, None) {
                let rect = D2D_RECT_F { left: 0.0, top: 0.0, right: size.0, bottom: size.1 };
                dc.FillRectangle(&rect, &brush);
            }
        }
    }

    fn set_prop(&mut self, name: &str, value: Option<&JsonValue>) {
        if name == "on" {
            self.on = value.and_then(|v| v.as_bool()).unwrap_or(false);
            self.context.set_size(120.0, 40.0);
            self.context.redraw();
        }
    }

    fn command(&mut self, name: &str, args: &JsonValue) {
        // optional
    }
}

fn register(app: &AppView) {
    app.register_component(
        "embed.badge",
        Rc::new(|context: ComponentContext| -> Box<dyn NativeComponent> {
            Box::new(Badge { on: false, context })
        }),
    );
}

يعمل draw مع كل إطار من النافذة، في إحداثيات العرض (بالنقاط)، ومقصوصًا على حدوده؛ استدعِ ComponentContext::redraw عندما يتغير ما يعرضه. يُغطّى المكوّن المرسوم ويُقصّ ويُحوَّل مثل أي عرض، وتصل اللمسات إلى حاويته. أما المكوّن الذي يعيد نافذة فرعية من window فتُنقل وتُقصّ وتُخفى مع عرضها، وتتلقى مدخلاتها الخاصة، وتظهر فوق كل ما يرسمه NSH (ويُخفى محتوى NSH الذي فوقها، كما هو الحال مع عروض الويب). ويعمل ComponentContext::emit وset_size (بالنقاط) وredraw من أي خيط. سجّل قبل AppView::load؛ فالسجلّ يخص AppView واحدًا (والعروض المنشأة من AppOptions نفسه).

ويتلقى المكوّن أيضًا عجلة الفأرة وتمريرات لوحة اللمس فوقه عبر wheel (يستخدمها عارض الصور للتكبير). ويحتوي Wheel الذي يصله على المؤشر (x، y، بنقاط المكوّن)، والمسافة بالنقاط (dx، dy، موجبة نحو اليمين والأسفل، كما تحرّك عروض التمرير محتواها) وبفواصل عجلة الفأرة (notches_x، notches_y، مع كسور من لوحات اللمس الدقيقة وعجلات الدقة العالية)، وما إذا كان قد جاء بخطوات دقيقة (precise) وأزرار التعديل المضغوطة (shift، ctrl، alt). ويعيد true للتمريرة التي أخذها؛ وتتمرر عروض التمرير المحيطة به مع البقية، وبشكل افتراضي مع جميعها. أما التمريرة التي بدأها عرض تمرير فتبقى على ذلك العرض. وCtrl + العجلة هو أيضًا قرص لوحة اللمس الدقيقة، ويصل Shift + العجلة كما أرسلته العجلة (dy، مع shift):

use nsh_platform_windows::Wheel;

impl NativeComponent for ImageView {
    // draw, set_prop, ...

    fn wheel(&mut self, event: &Wheel) -> bool {
        if !event.ctrl {
            return false; // the scroll views around it scroll
        }
        // √2 per notch, about the pointer.
        let factor = 2f32.powf(-event.notches_y / 2.0);
        self.zoom_about(event.x, event.y, factor);
        self.context.redraw();
        true
    }
}

#التسجيل من حزمة التطبيق

التطبيق الذي لديه حزمة Rust خاصة به (nsh.json "rust"، Rust) يسجّل المكوّنات الأصلية في تلك الحزمة، فتعمل في المضيفات التي يبنيها NSH للتطبيق: مضيف التطوير الخاص بـ nsh run وnsh test --target، و التطبيق الذي ينشئه nsh package. تسجّلها دالة مع #[native_components(<platform>)] في جذر الحزمة (lib.rs) على Components الخاص بتلك المنصة، ويستدعيها المضيف لكل عرض ينشئه، كما يسجّل التطبيق المضمِّن على AppView الخاص به:

use std::rc::Rc;
use nsh_core::prelude::*;

#[native_components(windows)]
fn components(components: &nsh_platform_windows::Components) {
    use nsh_platform_windows::{ComponentContext, NativeComponent};
    components.register(
        "embed.badge",
        Rc::new(|context: ComponentContext| -> Box<dyn NativeComponent> {
            Box::new(Badge { on: false, context })
        }),
    );
}

المنصة هي windows أو macos أو linux أو ios، والمكوّنات هي مكوّنات nsh_platform_windows أو nsh_platform_macos أو nsh_platform_linux أو nsh_platform_ios (الأقسام أعلاه). لا تُترجَم الدالة إلا على منصتها، لذا يكون في الحزمة واحدة لكل منصة تدعمها، وكل منها تسجّل الأسماء نفسها، وتعتمد على حزمة كل منصة لتلك المنصة فقط، بالإصدار نفسه ومن المصدر نفسه الخاصين بحزم NSH الأخرى (nsh-core وبقيتها؛ وتتحقق واجهة سطر الأوامر من تطابقها جميعًا):

[target.'cfg(windows)'.dependencies]
nsh-platform-windows = { version = "0.3", default-features = false }

[target.'cfg(target_os = "macos")'.dependencies]
nsh-platform-macos = { version = "0.3", default-features = false }

يسجّل المضيف مكوّنات الحزمة عند إنشاء كل عرض، قبل أن يسجّل الكود الذي يحتفظ بالعرض مكوّناته الخاصة: فالاسم المسجَّل على العرض بعد ذلك (AppView::register_component) يستبدل اسم الحزمة.

#القواعد والحدود

  • سجّل قبل التحميل. تُطبَّق المصانع (Factories) على العناصر التي تُنشأ بعد التسجيل. سجّل كل اسم قبل loadBundle / loadAssetBundle / run، حتى يجدها الإطار الأول.
  • الحاوية تملك الزخرفة واللمس. الخلفية والحدود ونصف قطر الزوايا والظلال والشفافية والتحويلات و overflow: "hidden" تُطبَّق على الحاوية التي يرسمها NSH. يملأ العرض الخاص بك هذه الحاوية ويستقبل اللمسات التي تقع داخلها.
  • الإمكانات (Capabilities) لا تنطبق. المكوّن المسجَّل هو شيفرة التطبيق المضيف نفسه؛ والإمكانات التي يعلنها التطبيق لا تقيّد ما يفعله. إذا كنت تضمّن تطبيقات لا تثق بها، فسجّل فقط المكوّنات الآمنة لاستخدامها.
  • أبقِ الواجهة قائمة على البيانات. تنتقل الخصائص (Props) وحمولات الأحداث ووسائط الأوامر على هيئة نص JSON؛ أرسل المعرّفات والقيم الصغيرة، لا الكتل الكبيرة، مع كل تغيير.
  • حزمة واحدة لكلا المنصتين. سجّل الأسماء نفسها بالخصائص والأحداث نفسها على iOS وAndroid، لتعمل حزمة واحدة على كليهما.
  • الأسماء تحت nsh/ تخص NSH. مكوّنات NSH الخاصة (شريط التبويب الخاص بالنظام في TabBar التابع لـnsh/mobile) مسجَّلة تحت nsh/؛ فسمِّ مكوّناتك باسم تطبيقك أو شركتك (embed.badge)، وأي تسجيل تحت nsh/ يسجّل تحذيرًا ويُبقي على مكوّن NSH.

#انظر أيضًا