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

التوثيق

عرض الويب والفيديو

يضمّن عارض الويب ومشغّل الفيديو محرّك الويب ومشغّل الوسائط الخاصين بالمنصة كعناصر عرض عادية: web_view وvideo في Rust (nsh_core::prelude)، وWebView وVideo من nsh في TSX. ولأن NSH يرسم باستخدام عناصر عرض أصلية، فإنها توضع في تخطيطك مثل أي عنصر آخر: تتمرر مع عنصرها الأب، وتقبل الزوايا المستديرة والقص، ويمكن لعناصر العرض الأخرى أن تتراكب فوقها دون مشاكل في الطبقات.

المكوّنiOSAndroidmacOSLinuxWindows
WebViewWKWebViewandroid.webkit.WebViewWKWebViewWebKitWebView من WebKitGTK 6.0WebView2 في نافذة فرعية
VideoAVPlayerLayer؛ AVPlayerViewController مع controlsMediaPlayer على TextureView؛ MediaController مع controlsAVPlayerLayer؛ AVPlayerView من AVKit مع controlsGTK’s media stream (GStreamer) في GtkPicture؛ GtkVideo مع controlsمحرّك الوسائط في Media Foundation، وتُرسم الإطارات بواسطة Direct2D؛ وشريط التحكم الخاص بـ NSH مع controls

تُحمَّل الأطر التي تقف خلفها (WebKit وAVFoundation) عند أول استخدام لأحدها في التطبيق، فلا تتحمل التطبيقات التي لا تعرض صفحة ويب أو فيديو كلفتها عند الإقلاع.

على macOS لا يملك عارض الويب عنصر تمرير خاصاً به: مع scrollable={false} تمرّر عجلة الفأرة ولوحة اللمس عناصر التمرير في التطبيق المحيطة به بدلاً من الصفحة، التي لا تزال تتمرر بلوحة المفاتيح وبنصوصها البرمجية الخاصة.

على Linux يكون عارض الويب صفحة WebKitGTK تُرسم داخل عنصر العرض الخاص بها: أي محتوى NSH فوقها (مثل حوار أو ورقة) يغطيها، وعناصرها الأب تقصّها. وتنطبق الصلاحية webview هناك أيضاً على تنقلات الإطارات (يبقى iframe لمضيف غير مسموح به فارغاً)، ويمنع scrollable: false الصفحة نفسها من التمرير (أما العناصر داخلها فتواصل التمرير). تحتاج التطبيقات إلى WebKitGTK 6.0 مثبّتاً؛ أما بيئة التشغيل المبنية بدونه فتعرض عنصراً نائباً محايداً. تشغّل مقاطع الفيديو الصيغ التي تفك ترميزها إضافات GStreamer المثبّتة؛ وrate غير مدعوم (تُشغَّل بسرعة 1×)، وonEnd لا يتوفر إلا لمقاطع الفيديو التي لا تتكرر.

على Windows يكون عارض الويب عنصر تحكم WebView2 في نافذة فرعية تتبع عنصر العرض الخاص به: يُقصّ بحسب عناصره الأب وزواياه المستديرة، لكن محتوى NSH المرسوم فوقه (مثل حوار أو ورقة) يبقى تحته. ويحتاج إلى WebView2 Runtime (وهو جزء من Windows 11؛ ويُثبَّت مع Edge على Windows 10)، وإلا فإنه يعرض عنصراً نائباً؛ وbaseUrl لـ HTML وscrollable وallowsBackForwardNavigationGestures غير مدعومة. أما مقاطع الفيديو فهي محتوى عادي (تُقصّ وتُحوَّل ويُغطّى عليها كأي عنصر عرض) وتشغّل الصيغ التي تفك ترميزها برامج الترميز المثبّتة في Media Foundation؛ وrate يعمل، ولا توجد صورة غلاف، ويظهر شريط التحكم دائماً ما دام controls مفعّلاً.

#WebView

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    web_view("https://en.m.wikipedia.org/wiki/Swift_(programming_language)").flex_1()
}

#المصادر

يعرض عارض الويب أحد src وhtml:

الخصائصيعرض
src="https://…"صفحة ويب.
html="<!doctype html>…"HTML من سلسلة نصية، يُعرض بدلاً من src.
html مع baseUrl="https://example.com/"HTML تُحلّ عناوين URL النسبية فيه مقابل baseUrl.

في Rust يعرض web_view(url) صفحة ويعرض web_view_html(html, base_url) شيفرة HTML (base_url قيمة من النوع Option<&str>)؛ ويمكن أن يكون عنوان URL وشيفرة HTML إشارة (signal) أو دالة مغلقة (closure)، فتُحمَّل الصفحة من جديد عندما يتغير ما تقرؤه. يؤدي تغيير src أو html إلى تحميل الصفحة الجديدة.

#الخصائص

في Rust تكون كل خاصية دالة بناء (builder) بصيغة snake case (injected_java_script وjava_script_enabled وscrollable وuser_agent؛ وinjected_java_script_before_content لـ injectedJavaScriptBeforeContentLoaded، وback_forward_gestures لـ allowsBackForwardNavigationGestures في iOS) وكل حدث دالة on_… (on_navigation_state_change) يتلقى مستمعها WebNavigation (url وtitle وloading وcan_go_back وcan_go_forward) أو، في حالة on_error وon_message، &str.

الخاصيةالنوعالافتراضيالوصف
srcstringعنوان URL للصفحة المراد عرضها.
htmlstringشيفرة HTML مضمّنة، تُعرض بدلاً من src.
baseUrlstringما تُحلّ عناوين URL النسبية في html مقابله.
injectedJavaScriptstringيعمل بعد تحميل كل صفحة.
injectedJavaScriptBeforeContentLoadedstringيعمل قبل النصوص البرمجية الخاصة بكل صفحة.
javaScriptEnabledbooleantrueتشغّل الصفحات النصوص البرمجية.
scrollablebooleantrueيستطيع المستخدم تمرير الصفحة.
userAgentstringسلسلة user agent الخاصة بالمتصفح.
ios={{ allowsBackForwardNavigationGestures }}booleaniOS: التمرير بالسحب للخلف وللأمام في سجل الصفحات.
desktop={{ allowsBackForwardNavigationGestures }}booleanmacOS: السحب بإصبعين على لوحة اللمس للرجوع والتقدم؛ Windows: السحب على شاشة اللمس؛ Linux: السحب على لوحة اللمس.

الأحداث:

الحدثالبيانات المرسلةمتى
onLoadStartWebViewNavigationبدأت صفحة بالتحميل.
onLoadWebViewNavigationانتهت صفحة من التحميل.
onError{ message }فشل التحميل.
onNavigationStateChangeWebViewNavigationتغيّر عنوان URL أو العنوان أو حالة التحميل أو السجل.
onMessage{ data: string }أرسلت الصفحة رسالة (أدناه).

WebViewNavigation هو { url, title, loading, canGoBack, canGoForward }.

#متصفح مع الرجوع والتقدم

يوفّر مرجع عارض الويب (element_ref() يوضع عليه بواسطة .element_ref(&web) في Rust وref في TSX) الدوال reload وgo_back / goBack وgo_forward / goForward وstop_loading / stopLoading وinject_java_script / injectJavaScript(script) وpost_message / postMessage(data) وsend_input / sendInput(input) (أدناه):

use nsh_core::prelude::*;

pub fn browser() -> impl IntoElement {
    let web = element_ref();
    let nav = signal(None::<WebNavigation>);
    let button = move |label: &'static str, enabled: fn(&WebNavigation) -> bool, action: fn(&ElementRef)| {
        let enabled = move || nav.with(|n| n.as_ref().is_some_and(enabled));
        text(label)
            .p(px(12.0))
            .text_color(move || if enabled() { rgb(0x0969da) } else { rgb(0x8c959f) })
            .disabled(move || !enabled())
            .on_press(move || action(&web))
    };
    view()
        .flex_1()
        .pt(px(50.0))
        .child(
            text(move || {
                nav.with(|n| match n {
                    Some(n) if n.loading => "Loading…".to_owned(),
                    Some(n) => n.title.clone(),
                    None => String::new(),
                })
            })
            .number_of_lines(1)
            .px(px(12.0))
            .text_color(rgb(0x57606a)),
        )
        .child(
            web_view("https://developer.mozilla.org/")
                .element_ref(&web)
                .flex_1()
                .on_navigation_state_change(move |n: &WebNavigation| nav.set(Some(n.clone())))
                .back_forward_gestures(true),
        )
        .child(
            view()
                .flex_row()
                .justify_around()
                .child(button("Back", |n| n.can_go_back, |web| web.go_back()))
                .child(button("Forward", |n| n.can_go_forward, |web| web.go_forward()))
                .child(button("Reload", |_| true, |web| web.reload())),
        )
}

#تبادل الرسائل بين الصفحة والتطبيق

تتحدث الصفحات إلى التطبيق باستخدام window.nsh.postMessage(string)، وتصل على شكل on_message (onMessage). ويتحدث التطبيق إلى الصفحة باستخدام post_message الخاصة بالمرجع (postMessage(string))، وتصل في الصفحة على شكل حدث message على window. الرسائل سلاسل نصية؛ أرسل JSON للبيانات المنظَّمة.

use nsh_core::prelude::*;

const PAGE: &str = r#"<!doctype html>
<html><head><meta name="viewport" content="width=device-width"></head>
<body style="font-family: -apple-system, sans-serif; margin: 16px">
  <button onclick="window.nsh.postMessage(JSON.stringify({ type: 'hello', at: Date.now() }))">
    Post to the app
  </button>
  <p id="got">Nothing from the app yet.</p>
  <script>
    window.addEventListener("message", (e) => {
      document.getElementById("got").textContent = "The app says: " + e.data;
    });
  </script>
</body></html>"#;

#[component]
pub fn App() -> impl IntoElement {
    let web = element_ref();
    let last = signal(String::new());
    view()
        .flex_1()
        .pt(px(50.0))
        .gap(px(12.0))
        .child(
            web_view_html(PAGE, None)
                .element_ref(&web)
                .h(px(240.0))
                .on_message(move |data: &str| {
                    let message: serde_json::Value = serde_json::from_str(data).unwrap_or_default();
                    last.set(message["type"].as_str().unwrap_or_default().to_owned());
                }),
        )
        .child(
            text("Post to the page")
                .p(px(16.0))
                .text_color(rgb(0x0969da))
                .on_press(move || web.post_message("hi from nsh")),
        )
        .child(text(move || format!("Last message: {}", last.get())).px(px(16.0)))
}

تشغّل inject_java_script الخاصة بالمرجع (injectJavaScript(script)) شيفرة في الصفحة الحالية في أي وقت؛ أما injected_java_script وinjected_java_script_before_content (injectedJavaScript، injectedJavaScriptBeforeContentLoaded) فتعملان مع كل تحميل لصفحة، قبل النصوص البرمجية الخاصة بالصفحة أو بعدها، وهما المكان المناسب لتثبيت الخطافات (hooks):

web_view("https://example.com")
    .injected_java_script_before_content("window.isNativeApp = true;")
    .injected_java_script("window.nsh.postMessage(document.title);")
    .on_message(|title: &str| log::info!("title {title}"))

#إدخال كأنه من المستخدم نفسه

لا يستطيع injectJavaScript سوى إنشاء أحداث اصطناعية، ترى الصفحة أنها غير موثوقة (event.isTrusted هو false) وتتجاهلها بعض عناصر الواجهة. أما send_input الخاصة بالمرجع (sendInput(input)) فتمنح الصفحة إدخالاً كما لو كان من يدَي المستخدم نفسه، فتكون أحداثه موثوقة:

  • { type: "click", x, y } ينقر على نقطة في إطار العرض، بوحدات بكسل CSS (ما يبلغ عنه getBoundingClientRect()).
  • { type: "text", text } يكتب النص في العنصر الذي يملك تركيز الصفحة، مفتاحاً بعد مفتاح: كل حرف على مفتاحه في تخطيط US، وفاصل السطر على أنه Enter، وما لا يكتبه أي مفتاح في تخطيط US (é وCJK والرموز التعبيرية) دون مفتاح (code هو "Unidentified").
  • { type: "key", key } يضغط Enter أو Tab أو Escape أو Backspace.
use nsh_core::{WebInput, WebKey, prelude::*};
use std::time::Duration;

fn search(web: &ElementRef) {
    web.send_input(WebInput::Click { x: px(120.0), y: px(40.0) });
    let web = *web;
    set_timeout(Duration::from_millis(50), move || {
        web.send_input(WebInput::Text("espresso".into()));
        web.send_input(WebInput::Key(WebKey::Enter));
    });
}

في Rust هذه هي WebInput::Click { x, y } وWebInput::Text(text) وWebInput::Key(WebKey::Enter).

يحمل الإدخال الموثوق تفعيل المستخدم (user activation)، أي أنه قد يفتح نوافذ، ويكتب في الحافظة، ويفتح لوحة الملفات في النظام، ويضغط زر التأكيد في صفحة، ولذلك فهو صلاحية قائمة بذاتها: يحتاج التطبيق إلى "webview": { "input": true } في capabilities ضمن nsh.json (webview)، حتى عندما لا يعلن عن أي صلاحيات، وإلا أسقطت النواة الاستدعاء.

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

قبل تقديم أي شيء يعتمد عليه، اسأل هل يعمل هنا: يُرجع web_input_available() (WebView.inputAvailable()) القيمة true فقط حيث تسلّم المنصة الإدخال ويملك التطبيق webview.input، فهو false على المنصات التي تتجاهل الاستدعاء وفي التطبيقات التي لا تملك هذه الصلاحية.

use nsh_core::prelude::*;

let input = signal(false);
spawn(async move { input.set(web_input_available().await) });

#المواقع التي يجوز لعارض الويب عرضها

إذا أعلن nsh.json الخاص بك عن "capabilities"، فلا يجوز لعارض الويب أن يعرض أو يتنقل إلا إلى المضيفين المدرجين ضمن webview:

{
  "capabilities": {
    "webview": { "hosts": ["example.com", "*.example.com"] }
  }
}

يشمل الفحص src الخاص بعارض الويب وكل تنقل من المستوى الأعلى تجريه الصفحة (الروابط وإعادة التوجيه وتغييرات location)؛ ويُحسم أصلياً عندما تسأل المنصة هل يجوز للتنقل أن يمضي. أما الموارد الفرعية للصفحة (النصوص البرمجية والصور والإطارات) فهي شأن محتوى الويب ولا تُفحص. يطابق *.example.com النطاقات الفرعية فقط؛ أدرج example.com أيضاً للنطاق المجرد. أما التطبيق الذي لا يحتوي على مفتاح "capabilities" فيجوز له عرض أي صفحة. راجع العزل والصلاحيات.

#Video

use nsh_core::prelude::*;

pub fn player() -> impl IntoElement {
    let player = element_ref();
    let paused = signal(true);
    let time = signal(String::new());
    let link = |label: &'static str| text(label).text_color(rgb(0x0969da));
    view()
        .flex_1()
        .justify_center()
        .p(px(16.0))
        .gap(px(12.0))
        .child(
            video("https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4")
                .element_ref(&player)
                .paused(paused)
                .repeat(true)
                .object_fit(ResizeMode::Contain)
                .h(px(220.0))
                .rounded(px(12.0))
                .overflow_hidden()
                .bg(rgb(0x000000))
                .on_progress(move |e: &VideoProgress| {
                    time.set(format!("{:.1} / {:.1} s", e.current_time, e.duration))
                })
                .on_end(move || paused.set(true)),
        )
        .child(
            view()
                .flex_row()
                .gap(px(16.0))
                .items_center()
                .child(
                    text(move || if paused.get() { "Play" } else { "Pause" })
                        .text_color(rgb(0x0969da))
                        .font_weight(FontWeight::SEMIBOLD)
                        .on_press(move || paused.update(|p| *p = !*p)),
                )
                .child(link("Restart").on_press(move || player.seek(0.0)))
                .child(text(move || time.get()).text_color(rgb(0x57606a))),
        )
}

يُتحكَّم في التشغيل بالخصائص: اضبط paused وmuted وvolume وrate من إشارات (signals) فيتبعها المشغّل. ويحتوي المرجع على seek(seconds).

في Rust، ينشئ video(url) المشغّل (عنوان URL قيمة أو إشارة أو دالة مغلقة) وكل خاصية دالة بناء بصيغة snake case (paused وrepeat(true) وobject_fit(ResizeMode::Contain) (نمط objectFit) وprogress_interval لـ progressUpdateInterval)؛ ويتلقى on_load VideoInfo (duration، size)، ويتلقى on_progress VideoProgress (current_time، duration)، ويتلقى on_buffer bool.

الخاصيةالنوعالافتراضيالوصف
srcstringعنوان URL للفيديو.
pausedbooleanfalseالتشغيل متوقف مؤقتاً.
mutedbooleanfalseبلا صوت.
volumenumber1من 0 إلى 1.
ratenumber1سرعة التشغيل.
repeatbooleanfalseيعيد التشغيل من البداية عند النهاية.
objectFit (نمط)، وفئات object-*"cover" | "contain" | "fill" | "none" | "scale-down""contain"كيفية ملاءمة الصورة لعنصر العرض، مثل object-fit في CSS (class="object-cover").
controlsbooleanfalseعناصر التحكم في التشغيل الخاصة بالمنصة.
progressUpdateIntervalnumber250الميلي ثانية بين أحداث onProgress.
الحدثالبيانات المرسلةمتى
onLoadStartبدأ التحميل.
onLoad{ duration, size }أصبح الفيديو جاهزاً: طوله بالثواني وحجمه.
onProgress{ currentTime, duration }أثناء التشغيل، كل progressUpdateInterval ms.
onBuffer{ isBuffering }توقف التشغيل في انتظار البيانات، أو استؤنف.
onEndوصل التشغيل إلى النهاية.
onError{ message }فشل التحميل أو التشغيل.

مع controls ترسم المنصة زر التشغيل وشريط التنقل وزر ملء الشاشة الخاصة بها. وبدونه، ارسم عناصر التحكم الخاصة بك باستخدام عناصر عرض فوق الفيديو أو تحته؛ ولأن الفيديو عنصر عرض أصلي في تخطيطك، تعمل التراكبات بشكل طبيعي:

view()
    .h(px(220.0))
    .child(video(url).paused(paused).flex_1())
    .child(
        view()
            .absolute()
            .inset_0()
            .items_center()
            .justify_center()
            .on_press(move || paused.update(|p| *p = !*p))
            .child(when(move || paused.get(), || {
                text("▶").text_size(px(48.0)).text_color(rgb(0xffffff))
            })),
    )

مصادر الفيديو هي طلبات شبكة: ومع الصلاحيات المعلنة، يجب السماح بمضيف الفيديو ضمن network في nsh.json، كما هو الحال مع fetch والصور البعيدة. المصدر المرفوض لا يعرض شيئاً، ويُسجَّل الرفض مرة واحدة مع المفتاح الذي ينبغي إضافته.

#مراجع ذات صلة