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

العناصر

Image

Image يعرض صورة من أصول التطبيق (assets) أو من عنوان URL. تُنزَّل الصور وتُفكّ ترميزها خارج الخيط الرئيسي، وتُصغَّر إلى الحجم الذي يعرضها به العنصر، وتُخزَّن مؤقتًا في الذاكرة. تُشغَّل صور GIF المتحركة وWebP وAPNG. في Rust تُنشأ الصورة بـ image(src)، وفي TSX بـ <Image> من nsh.

el-image.tsx
el-image.tsxLive
import { Image, mount, signal, Text, View } from "nsh";
import { windowSize } from "nsh";
import { button, buttonText, focusRing, ui } from "./palette";

// The pictures are SVG `data:` URIs, so the demo needs no network. An app
// would import a file next to it (`import hills from "./hills.png"`) or pass
// a URL.
const svg = (body: string, width: number, height: number) =>
  `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`)}`;

const landscape = svg(
  `<defs><linearGradient id="s" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#7ea8ff"/><stop offset="1" stop-color="#ffd59e"/></linearGradient></defs>
  <rect width="1200" height="800" fill="url(#s)"/>
  <circle cx="880" cy="250" r="90" fill="#fff4c2"/>
  <path d="M0 560 Q 250 380 520 520 T 1200 470 V800 H0Z" fill="#6aa86b"/>
  <path d="M0 650 Q 330 520 700 640 T 1200 610 V800 H0Z" fill="#3f7d4c"/>
  <text x="40" y="770" font-family="system-ui" font-size="40" fill="#ffffff">1200 × 800</text>`,
  1200,
  800,
);

const portrait = svg(
  `<defs><linearGradient id="n" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1b1f3a"/><stop offset="1" stop-color="#b79cff"/></linearGradient></defs>
  <rect width="600" height="900" fill="url(#n)"/>
  <circle cx="300" cy="260" r="120" fill="#f4f1ff"/>
  <circle cx="350" cy="230" r="110" fill="#2a2f55"/>
  <path d="M0 700 L 180 520 L 330 660 L 450 560 L 600 700 V900 H0Z" fill="#141629"/>
  <text x="30" y="870" font-family="system-ui" font-size="36" fill="#ffffff">600 × 900</text>`,
  600,
  900,
);

// Shown until the picture loads, and in its place when it fails.
const placeholder = svg(
  `<rect width="120" height="80" fill="#8d9198" opacity="0.25"/><path d="M20 62 L48 34 L68 52 L80 42 L100 62Z" fill="#8d9198"/><circle cx="86" cy="24" r="8" fill="#8d9198"/>`,
  120,
  80,
);

// An icon drawn in one color: `tintColor` recolors it.
const star = svg(`<path d="M12 2l3 6.5 7 .8-5.2 4.8 1.5 7L12 17.6 5.7 21.1l1.5-7L2 9.3l7-.8z" fill="#000"/>`, 24, 24);

const sources = { landscape, portrait, missing: "asset:/missing.png" } as const;
type Source = keyof typeof sources;
const modes = ["cover", "contain", "fill", "none"] as const;
type Mode = (typeof modes)[number];

function Segments<T extends string>(props: { options: readonly T[]; value: T; onChange: (v: T) => void }) {
  return (
    <View style={{ ...ui().inset, flexDirection: "row", padding: 3, borderRadius: 10 }}>
      <Each of={props.options}>
        {(option) => {
          const on = () => props.value === option();
          return (
            <View
              onPress={() => props.onChange(option())}
              style={{
                flex: 1,
                alignItems: "center",
                paddingVertical: 6,
                borderRadius: 8,
                backgroundColor: on() ? ui().raised.backgroundColor : "transparent",
                backgroundImage: on() ? ui().raised.backgroundImage : "none",
                boxShadow: on() ? ui().raised.boxShadow : "none",
              }}
              hoverStyle={{ opacity: 0.88 }}
              focusStyle={focusRing()}
              activeStyle={{ opacity: 0.7 }}
            >
              <Text style={{ fontWeight: "600", color: on() ? ui().text : ui().muted }}>{option()}</Text>
            </View>
          );
        }}
      </Each>
    </View>
  );
}

function App() {
  const source = signal<Source>("landscape");
  const mode = signal<Mode>("cover");
  const blur = signal(false);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  const wide = () => windowSize().width >= 700;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>image</Text>
      <View style={{ flex: 1, flexDirection: wide() ? "row" : "column", gap: 16 }}>
        <Image
          src={sources[source()]}
          placeholder={placeholder}
          blurRadius={blur() ? 12 : 0}
          fadeDuration={250}
          onLoadStart={() => log("onLoadStart")}
          onLoad={({ size }) => log(`onLoad ${Math.round(size.width)} × ${Math.round(size.height)}`)}
          onError={(e) => log(`onError ${e.message}`)}
          onLoadEnd={() => log("onLoadEnd")}
          style={{ ...ui().inset, flex: 1, minHeight: 180, borderRadius: 14, objectFit: mode() }}
        />
        <View style={{ gap: 12, width: wide() ? 320 : undefined }}>
          <Text style={{ color: ui().muted }}>src</Text>
          <Segments options={Object.keys(sources) as Source[]} value={source()} onChange={(s) => source.set(s)} />
          <Text style={{ color: ui().muted }}>objectFit</Text>
          <Segments options={modes} value={mode()} onChange={(m) => mode.set(m)} />
          <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
            <View onPress={() => blur.set(!blur())} {...button("secondary", { on: blur })}>
              <Text style={buttonText("secondary", { on: blur })}>blurRadius {blur() ? 12 : 0}</Text>
            </View>
            <Text style={{ color: ui().muted }}>tintColor</Text>
            <Image src={star} style={{ width: 24, height: 24, tintColor: ui().accent }} />
            <Image src={star} style={{ width: 24, height: 24, tintColor: ui().info }} />
          </View>
          <View style={{ ...ui().card, borderRadius: 12, padding: 12, gap: 4 }}>
            <Text style={{ color: ui().muted, fontSize: 12 }}>Events, newest first</Text>
            <Each of={events()}>
              {(line) => <Text style={{ color: ui().text, fontFamily: "monospace", fontSize: 13 }}>{line()}</Text>}
            </Each>
          </View>
        </View>
      </View>
    </View>
  );
}

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

// assets/logo.png in the app's crate.
const LOGO: ImageAsset = asset!("logo.png");

view()
    .flex_row()
    .gap_3()
    .child(image(LOGO))
    .child(
        image("https://example.com/avatar.jpg")
            .placeholder(LOGO)
            .object_fit(ResizeMode::Cover)
            .size(px(64.0))
            .rounded_full(),
    )

الأصل (asset) يعرف حجمه: فإذا لم تحدد عرضًا ولا ارتفاعًا أخذت الصورة حجمه، وإذا حددت أحدهما اتبعه الآخر وفق نسبة أبعاد الأصل. أما حجم الصورة من URL فلا يُعرف إلا بعد تحميلها، لذا حدّد للصور البعيدة عرضًا وارتفاعًا (أو نسبة أبعاد مع أحدهما).

#المصادر

يقبل المصدر (src وplaceholder؛ image(..) و.placeholder(..) في Rust) ما يلي:

القيمةمثالالوصف
أصل (asset)Rust asset!("logo.png")، TSX import logo from "./logo.png"ImageAsset { url, size, scales? } يُقاس حجمه بالنقاط: ملف @2x يُحسب بنصف عدد بكسلاته. عند وجود logo@2x.png / logo@3x.png بجانب logo.png يُحمَّل المتغير المناسب لنسبة بكسلات الشاشة (كثافات البكسل). يمكن استيراد .png و.jpg و.jpeg و.gif و.webp و.heic، وكذلك .svg بحجم يحدده width وheight أو viewBox (صور SVG).
عنوان URL"https://example.com/a.jpg"يُنزَّل ويُخزَّن مؤقتًا. تُشارَك عمليات تنزيل عنوان URL نفسه بين العناصر.
مسار أصل"asset:/images/a.png"ملف من أصول التطبيق.
معرّف URI من نوع data:"data:image/png;base64,iVBOR…"بايتات داخل المعرّف، بترميز base64 أو بالترميز النسبي المئوي (data:image/svg+xml,%3Csvg…).
عنوان URL من نوع blob:URL.createObjectURL(blob)بايتات Blob أو File (في TSX). تحتفظ الصورة بالبايتات: فإلغاء العنوان يترك صورة تُعرض كاملة.

تُعرض الصورة المبنية من بايتات يملكها التطبيق أصلًا دون أي طلب. فمحادثة تعرض صورة اختارها المستخدم من الـ blob الخاص بها أثناء رفعها، ثم تبدّل src إلى عنوان URL الخاص بالصورة:

// Bytes in a `data:` URI (an image the app made or received inline).
image(format!("data:image/png;base64,{encoded}")).size(px(64.0))

البايتات المتطابقة صورة واحدة: فالصورة نفسها بوصفها معرّف URI من نوع data: وبوصفها blob تُفكّ مرة واحدة وتتشارك الذاكرة المؤقتة.

أثناء التطوير تُقدَّم الأصول من خادم التطوير؛ ويقوم nsh build وnsh package بنسخها إلى التطبيق (وتشحن حزمة Rust المجلد assets/ الخاص بها مع registry.assets(assets!())). راجع الصور والأصول.

#الخصائص

إضافةً إلى الخصائص المشتركة (مع style من النوع ImageStyle):

الخاصيةRustالنوعالافتراضيالوصف
srcimage(src)string | ImageAssetالصورة.
placeholder.placeholder(src)string | ImageAssetتُعرض إلى أن يكتمل تحميل src؛ والأفضل أن تكون أصلًا محليًا.
blurRadius.blur_radius(px(..))numberتُطمِس الصورة بنصف القطر هذا بالنقاط (يُطبَّق مرة واحدة أثناء فك الترميز، خارج الخيط الرئيسي).
fadeDuration.fade_duration(ms)numberiOS 0، Android 300مدة ظهور الصورة المحمَّلة تدريجيًا، بالميلي ثانية.
tintColor (style).tint_color(..)ColorValueتعيد تلوين صورة القالب (أيقونة مرسومة بلون واحد).
objectFit (style).object_fit(ResizeMode::Contain)"cover" | "contain" | "fill" | "none" | "scale-down""cover"كيف تلائم الصورة صندوقها، كما في object-fit في CSS (فئات object-*): "cover" تملؤه وتقص الزائد، "contain" تلائمها داخله، "fill" تمطّها، "none" تعرضها بحجمها الطبيعي، "scale-down" مثلها لكنها تُصغَّر لتلائم الصندوق حين تكون أكبر منه.
objectPosition (style).object_position(..)، .object_top()، …string"center"موضع الصورة في صندوقها بعد أن يحدد objectFit حجمها، كما في object-position في CSS ("top"، "25% 75%"، "right 10px bottom 20%")؛ وتُقصّ الصورة المغطّية للصندوق حول هذا الموضع.
onLoadStart.on_load_start(..)() => voidبدأ التحميل.
onProgress.on_progress(..)(e: { loaded: number; total: number }) => voidتقدّم التنزيل بالبايتات (total تساوي 0 عندما يكون غير معروف)، مرة كل 100 ملّي ثانية على الأكثر.
onLoad.on_load(..)(e: { size: PixelSize }) => voidاكتمل التحميل؛ وحجم الصورة المصدر بالبكسل (لا حجم الصورة بعد فك الترميز والتصغير؛ وحجم SVG بالنقاط، وعلى الويب كما يقيسه المتصفح)؛ وفي حالة الصورة ذات المتغيرات، حجم المتغير المحمَّل.
onError.on_error(..)(e: { message: string; code?: string }) => voidفشل التحميل؛ وcode تكون "ERR_ACCESS_DENIED" لعنوان URL لا تمنحه قدرات التطبيق.
onLoadEnd.on_load_end(..)() => voidبعد onLoad أو onError.

ImageStyle يضيف tintColor إلى أنماط العنصر، وهي تعيد تلوين صورة القالب (أيقونة مرسومة بلون واحد)، وكذلك objectPosition الذي يضع الصورة في صندوقها (موضع الصورة).

image(ICON).size(px(24.0)).tint_color(rgb(0x0969da))

#الأحداث

الحدثRustالحمولةمتى
onLoadStart.on_load_startلا شيءبدأ التحميل.
onProgress.on_progress{ loaded, total } (ImageProgress)وصلت بايتات (مرة كل 100 ملّي ثانية على الأكثر).
onLoad.on_load{ size } (Size<DevicePixels>)عُرضت الصورة؛ وحجمها المصدر بالبكسل (وحجم SVG بالنقاط، وعلى الويب كما يقيسه المتصفح).
onError.on_error{ message, code? } (&str، الرسالة)فشل التحميل أو فك الترميز، أو أن المضيف خارج قدرات التطبيق (code "ERR_ACCESS_DENIED").
onLoadEnd.on_load_endلا شيءبعد onLoad أو onError.
let state = signal(SharedString::from("loading"));

image(url)
    .w_full()
    .aspect_ratio(16.0 / 9.0)
    .on_load(move |size: &Size<DevicePixels>| state.set(format!("{}×{}", size.width.0, size.height.0).into()))
    .on_error(move |message: &str| state.set(message.to_string().into()))

الصورة ذات متغيرات كثافة البكسل تحمّل أحد ملفاتها، و onLoad يبلّغ عن بكسلات ذلك الملف: 192×192 لـ logo@3x.png على شاشة 3×، في حالة logo.png بقياس 64×64 نقطة. وعندما تتغير نسبة البكسل وتحمّل الصورة متغيرًا آخر، فإنها تبلّغ عن ذلك التحميل أيضًا: onLoadStart، ثم onLoad مع حجم الملف الجديد (أو onError)، ثم onLoadEnd.

لمعرفة حجم الصورة أو لتحميلها قبل عرضها، يستدعي TSX ‏Image.getSize(src) وImage.prefetch(src) (فالـ Image نفسه الذي يرسم العنصر يحملهما؛ راجع واجهة Image البرمجية)، ويستدعي Rust ‏image_size(src) وprefetch_image(src) مع انتظارهما داخل مهمة (task). وعند تمرير صورة مستوردة ذات متغيرات، يحمّل Image.getSize وImage.prefetch المتغير الذي تعرضه هذه الشاشة، بينما يعيد getSize حجم الملف المستورد (64×64 لـ logo.png).

#دوال Ref

لا يوجد لهذا المكوّن دوال خاصة به.

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

  • يتضمن مفتاح الذاكرة المؤقتة حجم فك الترميز ونصف قطر الطمس: فعنوان URL نفسه المعروض بحجمين يُفكّ ترميزه مرتين لكنه يُنزَّل مرة واحدة.
  • يشغّل iOS الصور المتحركة كرسوم متحركة بالإطارات المفتاحية (حتى 600 إطار)؛ أما الصورة المتحركة الملوّنة فتعرض إطارها الأول.
  • يشغّل Android الصور المتحركة ابتداءً من API 28 (AnimatedImageDrawable)، دون تخزين مؤقت ودون طمس.
  • في Android تُرسم الحدود فوق الصورة، كما في iOS.