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

العناصر

View

View هو اللبنة الأساسية لكل شاشة: حاوية أصلية (UIView على iOS وMac Catalyst، وViewGroup على Android) تضع عناصرها الفرعية باستخدام flexbox أو grid، وترسم الخلفية والحدود والظلال والتدرجات اللونية، وتعالج الضغطات. الأزرار والبطاقات والصفوف والشاشات كلها views. في Rust يُنشأ باستخدام view()، وفي TSX باستخدام <View> من nsh.

el-view.tsx
el-view.tsxLive
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, Inspector, Panel, Stage, ui, wide } from "./palette";

function App() {
  const presses = signal(0);
  const size = signal("");
  const views = () => (
    <>
      <View
        onPress={() => presses.update((n) => n + 1)}
        {...button("accent", { align: "center" })}
      >
        <Text style={buttonText("accent")}>activeStyle opacity · pressed {presses()}×</Text>
      </View>

      <View
        onPress={() => presses.update((n) => n + 1)}
        activeStyle={{ backgroundColor: ui().border }}
        style={{ ...ui().card, backgroundImage: "none", borderRadius: 16, padding: 16 }}
        hoverStyle={{ opacity: 0.88 }}
        focusStyle={focusRing()}
      >
        <Text style={{ color: ui().text, fontSize: wide() ? 14 : 16 }}>activeStyle background ({desktop ? "click" : "tap"} and hold)</Text>
      </View>

      <View class="flex-row gap-3">
        <View class="flex-1 h-20 rounded-xl bg-[#ff6170] hover:bg-[#ff7d89] active:bg-[#d94d5b] items-center justify-center" focusStyle={focusRing()} onPress={() => {}}>
          <Text class="text-[#13160c] font-semibold">active:</Text>
        </View>
        <View class="flex-1 h-20 rounded-xl bg-[#45d49a] items-center justify-center shadow-lg">
          <Text class="text-[#0c0d0f] font-semibold">shadow-lg</Text>
        </View>
        <View
          class="flex-1 h-20 rounded-xl items-center justify-center"
          style={{ backgroundImage: `linear-gradient(135deg, ${ui().violet}, ${ui().info})` }}
        >
          <Text class="font-semibold" style={{ color: ui().page }}>gradient</Text>
        </View>
      </View>
    </>
  );
  const measured = (height?: number) => (
    <View
      onLayout={({ bounds }) => size.set(`${Math.round(bounds.size.width)} × ${Math.round(bounds.size.height)} pt`)}
      style={{
        flex: height === undefined ? 1 : undefined,
        height,
        borderRadius: 12,
        borderWidth: 2,
        borderStyle: "dashed",
        borderColor: ui().faint,
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Text style={{ color: ui().muted }}>onLayout: {size()}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>
            <Text style={{ color: ui().muted }}>Click and hold the views; resize the window to see onLayout</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={480} gap={16}>
                {views()}
                {measured(120)}
              </Stage>
              <Inspector>
                <Panel title="onPress">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>pressed {presses()}×</Text>
                </Panel>
                <Panel title="onLayout" grow>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{size()}</Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 640, alignSelf: "center", padding: 20, paddingTop: 24, gap: 16 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>
            {views()}
            {measured()}
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    let liked = signal(false);
    view()
        .flex_1()
        .p_6()
        .gap_4()
        .bg(rgb(0xf6f8fa))
        .child(
            view()
                .flex_row()
                .items_center()
                .gap_3()
                .p_4()
                .rounded(px(16.0))
                .bg(white())
                .shadow_md()
                .child(view().size(px(40.0)).rounded_full().bg(rgb(0x8250df)))
                .child(text("A card").flex_1().text_base())
                .child(
                    view()
                        .hit_slop(EdgeInsets::all(px(8.0)))
                        .active(|s| s.opacity(0.5))
                        .on_press(move || liked.update(|l| *l = !*l))
                        .child(text(move || if liked.get() { "♥" } else { "♡" }).text_size(px(22.0))),
                ),
        )
}

#التخطيط

يضع الـ view عناصره الفرعية باستخدام flexbox، بالقيم الافتراضية التالية: flexDirection: "column"، flexShrink: 0، alignContent: "flex-start"، position: "relative". يحوّل display: "grid" (.grid() في Rust) التخطيط إلى CSS grid، ويحوّله display: "block" إلى تخطيط الكتل. الأحجام بوحدة النقاط. راجع التخطيط و مرجع الأنماط.

يتبع display: "flex" سلوك CSS: ففي النمط، وفي الفئة flex وفي .flex() في Rust، يكون الاتجاه صفًا ما لم يُحدَّد اتجاه (flexDirection: "column"، flex-col، .flex_col()). أما الـ view الذي لا يحدد display فيرتب عناصره الفرعية في عمود.

#الخصائص

يملك View خاصية واحدة خاصة به، هي selectable (.selectable(true) في Rust): تُحدَّد النصوص داخل الـ view معًا كتحديد واحد (تحديد النص). أما بقية الخصائص فهي الخصائص المشتركة: الأنماط (style، class، classList؛ وطرائق Styled في Rust)، وdisabled، وخصائص الضغط (onPress، onLongPress، onPressDown، onPressUp، delayLongPress، hitSlop)، وأنماط الحالات (activeStyle، hoverStyle، focusStyle، disabledStyle، checkedStyle)، وonMouseEnter / onMouseMove / onMouseLeave، وonContextMenu، وonFileDragEnter / onFileDragLeave / onFileDrop، وfocusable، وonLayout، وref، وnativeID، وtestID، وخصائص الإيماءات (pan، pinch، rotation وأحداثها)، وخصائص اللمس والاستجابة، وخصائص إمكانية الوصول، وios / android / desktop.

#الـ views القابلة للتحديد

عند استخدام selectable، يمتد التحديد عبر عناصر Text وTextView داخل الـ view بترتيب القراءة؛ أما النصوص التي تحمل selectable={false} فتبقى خارج التحديد، والـ view القابل للتحديد داخل آخر يُعدّ جزءًا من الـ view الخارجي. راجع تحديد النص لمعرفة الإيماءات على كل منصة.

<View selectable class="gap-3 p-4">
  <Text>One selection runs from here…</Text>
  <Text>…to here.</Text>
</View>

#ردود الفعل عند الضغط

الـ view القابل للضغط هو أي view له معالج ضغط. وردّ الفعل عند الضغط يعود إلى التطبيق: نمط يُطبَّق أثناء الضغط ويُنفَّذ أصليًا ما دام الإصبع مضغوطًا، دون الرجوع إلى شيفرة التطبيق. وتؤدي hoverStyle وfocusStyle وdisabledStyle الدور نفسه للحالات الأخرى:

// Fade the whole view.
view().on_press(open).active(|s| s.opacity(0.6))

// Show a background; group_active styles the children.
view()
    .group()
    .on_press(open)
    .active(|s| s.bg(rgb(0xe5e7eb)))
    .child(text("Open").group_active(|s| s.opacity(0.7)))

// Change any style while pressed.
view().on_press(open).bg(rgb(0x2563eb)).active(|s| s.bg(rgb(0x1e40af)).scale(0.95))

على Android، تضيف android={{ ripple: { color: "#0000001f" } }} (.ripple(Ripple { .. }) في Rust) تأثير الموجة (ripple) الخاص بـ Material (وتعدّله borderless وradius وforeground).

يزيل disabled مستمعات الضغط من الـ view الأصلي (فلا ضغط ولا ردّ فعل) ويطبّق أنماط التعطيل:

view()
    .on_press(submit)
    .disabled(move || !valid.get())
    .bg(rgb(0x16a34a))
    .disabled_style(|s| s.opacity(0.4))

#الأحداث

الحدثRustالبيانات المرسلةمتى
onPress.on_pressPressEvent (لا شيء في Rust)انتهت نقرة داخل الـ view.
onLongPress.on_long_pressPressEvent (لا شيء في Rust)بقي الإصبع مضغوطًا مدة delayLongPress (500 مللي ثانية).
onPressDown / onPressUp.on_press_down / .on_press_upPressEvent (لا شيء في Rust)نزل إصبع على الـ view / رُفع أو غادره.
onMouseEnter / onMouseLeave.on_mouse_enter / .on_mouse_leaveMouseEventدخل الماوس إلى الـ view (أو إلى view داخله) / غادره أو غادر النافذة (التمرير بالمؤشر)؛ وقيمة onMouseLeave هي آخر موضع كان فيه.
onMouseMove.on_mouse_moveMouseEventتحرك الماوس فوق الـ view (أو فوق view داخله)، مع زر مضغوط أو بدونه، أو ضُغط مفتاح تعديل أو رُفع بينما يستقر المؤشر هناك دون أي زر مضغوط: position، pagePosition (قيم Point)، modifiers وpressedButton ("left"، "right"، "middle"، "back"، "forward" أو null)؛ مرة واحدة كحد أقصى في كل إطار.
onContextMenu.on_context_menuContextMenuEventضُغط الزر الثانوي فوق الـ view (القوائم السياقية).
onFileDragEnter / onFileDragLeave.on_file_drag_enter / .on_file_drag_leaveلا شيءدخلت عملية سحب ملفات محلية إلى وجهة سطح المكتب النشطة / غادرتها.
onFileDrop.on_file_drop{ uris: string[] } (FileDropEvent)أُسقطت ملفات أو مجلدات محلية على وجهة سطح المكتب.
onLayout.on_layout{ bounds } (Bounds<Points>)تغيّر الإطار بالنسبة إلى الأصل.
onPanStart / onPanEnd.on_pan_start / .on_pan_endPanEventبدأ / انتهى سحب مرتبط بـ pan؛ وتكون قيمة cancelled صحيحة (true) عندما يلغيه النظام.
onPinchStart / onPinchEnd.on_pinch_start / .on_pinch_end{ scale, velocity }بدأت / انتهت إيماءة قرص مرتبطة بـ pinch.
onRotationStart / onRotationEnd.on_rotation_start / .on_rotation_end{ rotation, velocity }بدأ / انتهى تدوير مرتبط بـ rotation.
onAccessibilityAction.on_accessibility_action{ actionName }نُفّذ إجراء إمكانية وصول مخصص.

أحداث اللمس والاستجابة: راجع الخصائص المشتركة.

#إسقاط الملفات على سطح المكتب

على AppKit وGTK وWin32 وGPUI، يستقبل الـ view الذي يملك onFileDrop الملفات والمجلدات المحلية المسحوبة من تطبيق آخر. يتتبع onFileDragEnter وonFileDragLeave الوجهة النشطة؛ ويستقبل onFileDrop { uris: string[] } بترتيب قائمة الملفات الأصلية. كل عنصر هو عنوان file: مطلق تُهرَّب فيه المسافات والمحارف غير ASCII. حلِّل العنوان للحصول على مسار نظام الملفات بدلًا من إزالة البادئة يدويًا.

let hovering = signal(false);
let files = signal(Vec::<String>::new());

view()
    .min_h(px(160.0))
    .p_6()
    .on_file_drag_enter(move || hovering.set(true))
    .on_file_drag_leave(move || hovering.set(false))
    .on_file_drop(move |event: &FileDropEvent| {
        files.set(event.uris.clone());
        hovering.set(false);
    })
    .child(text(move || if hovering.get() { "Release files here" } else { "Drop files here" }))
    .child(text(move || files.get().join("\n")))

أقرب سلف مفعَّل يستمع إلى onFileDrop هو من يستقبل الإسقاط. ويحدّ الجذر المشروط (modal) هذا البحث. يزيل disabled مستمعات سحب/إسقاط الملفات؛ ويؤدي الخروج والإلغاء والإكمال وإزالة الوجهة إلى مسح الحالة النشطة. يعتمد القبول على عملية Copy وعلى الحالة الأصلية المركّبة. ويقرر التطبيق ما إذا كان سيقرأ الملفات المستلمة أو يفتحها أو ينقلها.

#طرائق المرجع (Ref)

مرجع الـ view العادي هو ViewRef: focus() وblur() وmeasure(). يعطيك ref (.element_ref(&handle) في Rust) عقدته، مثلًا لتمريرها إلى setAccessibilityFocus (واجهة إمكانية الوصول الخاصة بـ nsh)، أو لقياسها (handle.measure() في Rust).

#الـ views داخل النص

الـ view الموضوع داخل نص هو مرفق مضمَّن: يستقر على خط الأساس ويلتف مع الكلمات، ويستقبل ضغطات خاصة به. راجع النص.

#ملاحظات حول المنصات

  • يرسم boxShadow على كل المنصات؛ أما android={{ elevation }} فيعطي ظل Material الخاص بـ Android بدلًا منه. وعلى Android يحتاج الظل إلى لون خلفية (فالـ view ذو الخلفية الشفافة لا يلقي ظلًا).
  • filter على الـ views: يدعم iOS opacity() وbrightness() حتى 1 وdrop-shadow() (وتقبل الصور كل مرشحات الألوان وblur())؛ ويدعمها Android كلها ابتداءً من API 31، ومرشحات الألوان فقط دون ذلك.
  • backdropFilter: "blur(…)" مادة نظام على iOS (تقريبية) ويتطلب API 31 على Android.
  • يرسم Android تحويلات translate وscale وrotate؛ أما التحويلات المائلة (skew()، skewX()، skewY()) فتُطبَّق على المنصات الأخرى.
  • يرسم borderCurve: "continuous" (.border_curve(BorderCurve::Continuous)) زوايا تنساب نحو الحواف (كما في أيقونات تطبيقات iOS)، على المنصتين.
  • يُطبَّق النمط cursor حيثما يوجد مؤشر (Mac Catalyst، وiPad مع مؤشر، وأجهزة سطح المكتب).