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

التوثيق

التخطيط

يرتّب NSH كل عنصر باستخدام Taffy، وهو تنفيذ بلغة Rust لتخطيطات CSS من نوع flexbox وgrid وblock، مضبوط بحيث يرصّ الأبناء عموديًا ولا ينكمشون افتراضيًا. يعمل التخطيط في النواة بعد كل دورة من شيفرتك، ولا تُرسل إلى العروض الأصلية إلا الإطارات التي تغيّرت. يقيس محرك النصوص في المنصة النصوصَ على الخيط نفسه، لذلك لا ينتظر التخطيط الخيط الرئيسي أبدًا.

guide-layout.tsx
guide-layout.tsxLive
import { mount, Scroll, signal, Text, View } from "nsh";
import { button, buttonText, desktop, Inspector, tint, ui, wide } from "./palette";

type Direction = "column" | "row";
type Justify = "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly";
type Align = "stretch" | "flex-start" | "center" | "flex-end";

function Choice<T extends string>(props: { label: string; options: readonly T[]; value: T; onChange: (v: T) => void }) {
  return (
    <View style={{ gap: 4 }}>
      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>{props.label}</Text>
      <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6 }}>
        <Each of={props.options}>
          {(o) => (
            <View onPress={() => props.onChange(o())} {...button("secondary", { size: "sm", on: () => props.value === o() })}>
              <Text style={buttonText("secondary", { on: () => props.value === o() })}>{o()}</Text>
            </View>
          )}
        </Each>
      </View>
    </View>
  );
}

function App() {
  const direction = signal<Direction>("row");
  const justify = signal<Justify>("flex-start");
  const align = signal<Align>("stretch");
  const colors = () => [ui().down, ui().info, ui().up];

  const box = (height: number) => (
    <View
      style={{
        height,
        padding: 10,
        gap: 8,
        borderRadius: 12,
        ...ui().inset,
        flexDirection: direction(),
        justifyContent: justify(),
        alignItems: align(),
      }}
    >
      <Each of={colors()}>
        {(c, i) => (
          <View style={{ minWidth: 44, minHeight: 44, padding: 6, borderRadius: 8, backgroundColor: c() }}>
            <Text style={{ color: ui().page, fontWeight: "700" }}>{i() + 1}</Text>
          </View>
        )}
      </Each>
    </View>
  );
  const more = () => (
    <>
      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>flex: 1 / 2 / fixed, percentages, aspectRatio</Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <View style={{ flex: 1, height: 36, borderRadius: 6, backgroundColor: tint(ui().info, 0.3) }} />
        <View style={{ flex: 2, height: 36, borderRadius: 6, backgroundColor: tint(ui().info, 0.6) }} />
        <View style={{ width: 36, height: 36, borderRadius: 6, backgroundColor: ui().info }} />
      </View>
      <View style={{ flexDirection: "row", gap: 8, alignItems: "flex-start" }}>
        <View style={{ width: "30%", aspectRatio: 1, borderRadius: 8, backgroundColor: tint(ui().warn, 0.35) }} />
        <View style={{ width: "40%", aspectRatio: 16 / 9, borderRadius: 8, backgroundColor: ui().warn }} />
      </View>

      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>grid</Text>
      <View style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6 }}>
        <View style={{ gridColumn: "span 2", height: 40, borderRadius: 6, backgroundColor: tint(ui().violet, 0.3) }} />
        <View style={{ height: 40, borderRadius: 6, backgroundColor: tint(ui().violet, 0.6) }} />
        <View style={{ gridRow: "span 2", borderRadius: 6, backgroundColor: ui().violet }} />
        <View style={{ gridColumn: "1 / 4", height: 40, borderRadius: 6, backgroundColor: tint(ui().violet, 0.8) }} />
      </View>

      <Text style={{ fontSize: 12, fontWeight: "600", color: ui().muted }}>position: absolute</Text>
      <View style={{ ...ui().inset, height: 100, borderRadius: 12 }}>
        <View style={{ position: "absolute", top: 8, left: 8, width: 60, height: 40, borderRadius: 6, backgroundColor: ui().up }} />
        <View style={{ position: "absolute", right: 8, bottom: 8, width: "40%", height: 30, borderRadius: 6, backgroundColor: ui().down }} />
        <View style={{ position: "absolute", top: 0, bottom: 0, left: "50%", width: 2, backgroundColor: ui().border }} />
      </View>
    </>
  );
  const justifyChoice = () => (
    <Choice
      label="justifyContent"
      options={["flex-start", "center", "flex-end", "space-between", "space-evenly"] as const}
      value={justify()}
      onChange={(v) => justify.set(v)}
    />
  );
  const alignChoice = () => (
    <Choice label="alignItems" options={["stretch", "flex-start", "center", "flex-end"] as const} value={align()} onChange={(v) => align.set(v)} />
  );

  return (
    <>
      {() =>
        wide() ? (
          <View style={{ flex: 1, backgroundColor: ui().page, flexDirection: "row", padding: 20, paddingTop: 24, gap: 16 }}>
            <Scroll style={{ flex: 1, minWidth: 0 }}>
              <View style={{ gap: 14 }}>
                {box(260)}
                {more()}
              </View>
            </Scroll>
            <Inspector>
              <Choice label="flexDirection" options={["row", "column"] as const} value={direction()} onChange={(v) => direction.set(v)} />
              {justifyChoice()}
              {alignChoice()}
            </Inspector>
          </View>
        ) : (
          <Scroll style={{ flex: 1, backgroundColor: ui().page }}>
            <View style={{ padding: 20, paddingTop: 24, gap: 14 }}>
              <View
                onPress={() => direction.set(direction() === "row" ? "column" : "row")}
                {...button("primary", { align: "flex-start" })}
              >
                <Text style={buttonText("primary")}>
                  flexDirection: {direction()} ({desktop ? "click" : "tap"} to switch)
                </Text>
              </View>
              {justifyChoice()}
              {alignChoice()}
              {box(220)}
              {more()}
            </View>
          </Scroll>
        )
      }
    </>
  );
}

mount(App);

خصائص التخطيط أدناه هي خصائص أنماط. في Rust هي دوال نمط مُنمَّطة بأنواع (.flex_row()، .gap_4()، .grid_cols(3)، .absolute().inset_0())؛ وفي TSX هي مفاتيح الخاصية style، ولكل منها فئة Tailwind (flex-row، gap-4، grid-cols-3، absolute inset-0، …). وكلاهما يُحوَّل إلى الأنماط الأصلية نفسها.

#Flexbox

يرتّب الـ View أبناءه باستخدام flexbox. الـ View الذي لا يضبط display يرصّ أبناءه في عمود؛ وdisplay: "flex" في نمط ما يجعله صفًّا ما لم تُضبط flexDirection، تمامًا كما في CSS وكما في فئة Tailwind ‏flex. عندما لا يضبط شيء هذه القيم، تُطبَّق القيم الافتراضية التالية:

الخاصيةالافتراضي في NSHالافتراضي في الويب
flexDirection"column" (View لا يضبط display)؛ "row" بمجرد ضبط display: "flex"row
flexShrink01
alignContent"flex-start" (الشبكات: "normal")normal
alignItems"stretch"stretch
minWidth / minHeight0auto
position"relative"static
view()
    .flex_row()
    .items_center()
    .gap_3()
    .p_4()
    .child(image(avatar).size(px(40.0)).rounded(px(20.0)))
    .child(
        view()
            .flex_1()
            .gap_0p5()
            .child(text(name).font_weight(FontWeight::SEMIBOLD))
            .child(text(message).text_color(rgb(0x6e7781)).number_of_lines(1)),
    )
    .child(text(time).text_color(rgb(0x6e7781)))
الخاصيةالقيمRust
flexDirection"column"، "row"، "column-reverse"، "row-reverse".flex_col()، .flex_row()، .flex_col_reverse()، .flex_row_reverse()
justifyContentالمحور الرئيسي: "flex-start"، "flex-end"، "start"، "end"، "center"، "space-between"، "space-around"، "space-evenly"، "stretch".justify_start()، .justify_center()، .justify_end()، .justify_between()، .justify_around()، .justify_evenly()
alignItemsالمحور المتقاطع: "stretch"، "flex-start"، "flex-end"، "start"، "end"، "self-start"، "self-end"، "center"، "baseline".items_stretch()، .items_start()، .items_center()، .items_end()، .items_baseline()
alignSelfمحاذاة ابن واحد على المحور المتقاطع؛ "auto" تتبع alignItems الخاصة بالأب.self_start()، .self_center()، .self_end()، .self_stretch()
alignContentأسطر الحاوية الملتفّة
flexWrap"nowrap"، "wrap"، "wrap-reverse".flex_wrap()
flexاختصار برقم واحد (أدناه).flex_1()، .flex_auto()، .flex_none()
flexGrow، flexShrink، flexBasisكما في CSS.flex_grow(n)، .flex_shrink(n)، .flex_shrink_0()، .basis(length)
gap، rowGap، columnGapالمسافة بين الأبناء.gap(length)، .gap_y(length)، .gap_x(length)، والخطوات .gap_2()، .gap_x_4()، …

flex-start وflex-end تتبعان اتجاه flex؛ أما start وend فتتبعان اتجاه الكتابة، كما في CSS (من اليمين إلى اليسار، start هو الجانب الأيمن)، وكذلك self-start / self-end الخاصتان بالابن نفسه.

flex هو اختصار CSS، مثل فئات Tailwind ‏flex-*: الرقم n يعني النمو بمقدار n، والانكماش بمقدار 1، والبدء من أساس قدره 0، وهكذا يتقاسم الأشقاء flex: 1 (.flex_1() في Rust، flex-1) المساحة بالتساوي مهما كان محتواهم؛ و"none" تعني 0 0 auto (يحتفظ الـ View بحجمه)، و"auto" تعني 1 1 auto، و"initial" تعني 0 1 auto، ويمكن كتابة النمو والانكماش والأساس صراحةً ("2 0 50%"، "1 1 120px"). ولأن الحدود الدنيا للأحجام تساوي 0 افتراضيًا، فإن flex: 0 (0 1 0) تُنقص حجم الـ View إلى لا شيء ما لم تُبقِ minWidth: "auto" على حجم محتواه.

view()
    .flex_row()
    .gap_2()
    // One share.
    .child(view().flex_1().h(px(36.0)).bg(rgb(0xc8e1ff)))
    // Two shares.
    .child(view().flex_grow(2.0).flex_shrink(1.0).basis(px(0.0)).h(px(36.0)).bg(rgb(0x79b8ff)))
    // Fixed.
    .child(view().size(px(36.0)).bg(rgb(0x0366d6)))

ولأن الحدود الدنيا للأحجام تساوي 0 افتراضيًا، فإن نص flex: 1 داخل صف ينكمش ويلتف بدلًا من دفع أشقائه إلى الخارج. أما minWidth: "auto" / minHeight: "auto" (min-w-auto / min-h-auto، .min_w(auto()) في Rust) فهي الحد الأدنى التلقائي في CSS: العنصر المنكمش يتوقف عند أصغر حجم لمحتواه، أي أطول كلمة في النص، كما في الويب.

#الأحجام

الخاصيةالقيمRust
width، heightنقاط، أو نسبة مئوية من الأب ("50%")، أو "auto".w(px(120.0))، .h(relative(0.5))، .w_full()، .size(px(44.0))، والخطوات .w_10()، …
minWidth، maxWidth، minHeight، maxHeightنقاط أو نسب مئوية؛ وminWidth / minHeight تقبلان أيضًا "auto"، أي أصغر حجم للمحتوى.min_w(..)، .max_w(..)، .min_h(..)، .max_h(..)
aspectRatioالعرض مقسومًا على الارتفاع: 1، 16 / 9.aspect_ratio(16.0 / 9.0)، .aspect_square()
padding، paddingHorizontal، paddingVertical، paddingTop، …نقاط أو نسب مئوية.p(..)، .px(..)، .py(..)، .pt(..)، …، والخطوات .p_4()، .px_2()، …
margin، marginHorizontal، marginVertical، marginTop، …نقاط أو نسب مئوية أو "auto".m(..)، .mx(..)، .my(..)، .mt(..)، …، .mx(auto())

في Rust يكون الطول مُنمَّطًا بنوع: px(16.0) تساوي 16 نقطة، وrelative(0.5) تساوي 50% من الأب، وauto() هي auto. تتبع دوال الخطوات مقياس المسافات في Tailwind بواقع 4 نقاط لكل خطوة (.p_4() تساوي 16 نقطة، و.gap_0p5() تساوي نقطتين).

بدون حجم، يكون الـ View بحجم محتواه (مضافًا إليه الحشو)، ويُمدَّد على المحور المتقاطع لأبيه بفعل alignItems: "stretch" الافتراضية. النسب المئوية نسبةً إلى حجم الأب (وفي الحشو والهامش، كما في CSS، نسبةً إلى عرضه).

aspectRatio يحدد البعد الآخر انطلاقًا من البعد المعروف:

view().w_full().aspect_ratio(16.0 / 9.0).bg(black())

للصور القادمة من أصول التطبيق حجم طبيعي، وتُرتَّب بهذا الحجم عندما لا يُحدَّد حجم (الصور والأصول). أما الصور البعيدة فلا حجم لها حتى تُحمَّل: حدّد لها حجمًا.

#Grid

display: "grid" (.grid() في Rust) يرتّب الأبناء في مسارات شبكة CSS:

view()
    .grid()
    .grid_template_columns("repeat(3, 1fr)")
    .gap_2()
    .child(view().col_span(2).h(px(60.0)).bg(rgb(0xd8b9ff)))
    .child(view().h(px(60.0)).bg(rgb(0xb083f0)))
    .child(view().row_span(2).bg(rgb(0x8250df)))
    .child(view().col_start(2).col_end(4).h(px(60.0)).bg(rgb(0x6639ba)))
الخاصيةالقيمRust
gridTemplateColumns، gridTemplateRowsقوائم مسارات CSS: "repeat(3, 1fr)"، "200px 1fr"، minmax()، auto-fill / auto-fit، وأسماء الخطوط.grid_cols(3)، .grid_rows(2)، .grid_template_columns("200px 1fr")، .grid_template_rows(..)
gridAutoColumns، gridAutoRowsمسارات الأعمدة والصفوف المُنشأة ضمنيًا
gridAutoFlow"row"، "column"، "dense"، "row dense"، "column dense".grid_flow_col()، .grid_flow_dense()
gridColumn، gridRow"1 / 3"، "span 2"، رقم خط أو اسمه: "content-start / content-end"، "2 col" (الخط الثاني المسمّى col)، "span col" (حتى الخط التالي المسمّى col).col_span(2)، .row_span(2)، .col_span_full()
gridColumnStart، gridColumnEnd، gridRowStart، gridRowEndأرقام الخطوط أو أسماؤها.col_start(1)، .col_end(3)، .row_start(..)، .row_end(..) (أرقام الخطوط)
justifyItems، justifySelfكيف تستقر العناصر في مناطقها على امتداد الصف: "start"، "end"، "center"، "stretch"، …
alignItems، alignSelfكيف تستقر العناصر في مناطقها على امتداد العمود.items_*()، .self_*()
justifyContent، alignContentكيف تستقر المسارات في الشبكة على امتداد الصف والعمود.justify_*()

تسمّي القوالب خطوطها بين أقواس مربعة، اسمًا واحدًا أو عدة أسماء لكل خط، وتوضع العناصر بحسب هذه الأسماء، كما في CSS (وفي Rust بحسب أرقام الخطوط):

view()
    .grid()
    .grid_template_columns("[full-start] 16px [content-start] 1fr [content-end] 16px [full-end]")
    // content-start / content-end: lines 2 to 3.
    .child(view().col_start(2).col_end(3).h(px(60.0)).bg(rgb(0xb083f0)))
    // full-start / full-end: lines 1 to 4.
    .child(view().col_start(1).col_end(4).h(px(60.0)).bg(rgb(0x8250df)))

في الشبكة، تكون القيمة الافتراضية لـjustifyContent وalignContent هي "normal" في CSS: المسارات ذات الحجم auto تتمدد لملء الحاوية، لذلك تتقاسم الشبكة التي لها ارتفاع وبلا قالب صفوف هذا الارتفاع بين صفوفها. أما القيمة المضبوطة صراحةً ("start"، "center"، "space-between"، …) فتضع المسارات كما تفعل في CSS. وتضبط فئات المحاذاة ما تضبطه Tailwind CSS بالضبط: content-start (align-content: flex-start) وplace-content-start تجمعان المسارات عند البداية، وcontent-normal وjustify-normal تمدّدانها، كما في المتصفح.

display: "block" يستخدم تخطيط block في CSS (الأبناء مرصوصون، وكلٌّ منهم بعرض الحاوية)، وdisplay: "none" (.hidden()) يزيل الـ View من التخطيط.

#التموضع المطلق

position: "absolute" (.absolute()) يُخرج الـ View من التدفق ويضعه بواسطة top وright وbottom وleft، نسبةً إلى أبيه:

view()
    .h(px(200.0))
    .child(image(cover).absolute().inset_0())
    .child(
        view()
            .absolute()
            .left_3()
            .right_3()
            .bottom_3()
            .child(text("Caption").text_color(white()).font_weight(FontWeight::BOLD)),
    )

تقبل الإزاحات نقاطًا ونسبًا مئوية (.top(..)، .right(..)، .bottom(..)، .left(..)، .inset(..) في Rust). الـ View الذي له left وright (أو top وbottom) وبلا حجم يتمدد بينهما. تُرسم الأشقاء اللاحقة فوق السابقة؛ وzIndex (.z_index(n)) يغيّر الترتيب.

#القياس

onLayout (.on_layout(..)) يُبلغ عن إطار الـ View، نسبةً إلى أبيه، بعد كل تخطيط تغيّر فيه:

let width = signal(0.0_f32);

view()
    .on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))
    .child(text(move || format!("{} points wide", width.get())))

الإطار بالنقاط: Bounds<Points> في Rust، و{ bounds } في TSX، وهو Bounds ({ origin: { x, y }, size: { width, height } }). لقراءة النافذة كاملةً، اقرأ window() في Rust أو windowSize() من nsh في TSX؛ وكلاهما يتبع التدوير وتغيير حجم النافذة، والنمط الذي يقرأهما يتغير مع النافذة:

view().style_when(|| window().width > px(600.0), |s| s.flex_row())

نقاط التوقف في Tailwind (sm:، md:، …) وportrait: / landscape: تعطي النتيجة نفسها بدون شيفرة.

#المناطق الآمنة

يغطي الـ View الجذر الشاشة كلها، تحت شريط الحالة ومؤشر الصفحة الرئيسية. أضف حشوًا للشاشات بمقدار حواف المنطقة الآمنة باستخدام env() في CSS (paddingTop: "env(safe-area-inset-top)"، وفي Tailwind ‏pt-[env(safe-area-inset-top)]، و.p_safe_area() في Rust؛ راجع المنطقة الآمنة)، أو اقرأ الحواف بواسطة safe_area() في Rust أو safeArea() / safeAreaInsets() في TSX وطبّقها بنفسك. أما صفحات مكدّس التنقل فتقع أصلًا تحت ترويستها.

#من اليمين إلى اليسار

يتبع اتجاه التخطيط في التطبيق لغة النظام، ويحدد النمط direction للـ View ("ltr" أو "rtl" أو "inherit"؛ و.direction(LayoutDirection::Rtl) في Rust) اتجاه شجرته الفرعية. عندما يُرتَّب التطبيق من اليمين إلى اليسار:

  • تمتد الصفوف من اليمين؛
  • تحتفظ left وright بجانبيهما، كما في المتصفح، في مفاتيح الأنماط والفئات على حد سواء: paddingLeft، marginLeft، left، borderLeftWidth، borderTopLeftRadius، textAlign: "left" وpl-4، ml-2، left-0، border-l، rounded-tl-lg، text-left تبقى على اليسار؛
  • تتبع الخصائص المنطقية الاتجاه، حيث تكون بداية السطر هي الجانب الأيمن: marginStart، marginEnd، paddingStart، paddingEnd، start، end، borderStartWidth، borderEndColor، borderTopStartRadius، …، textAlign: "start" / "end" (ps-4، ms-2، start-0، border-s، rounded-ss-lg، text-start كفئات؛ و.ms(..)، .me(..)، .ps(..)، .pe(..)، .border_s(..)، .rounded_ss(..)، … في Rust)؛ وmarginHorizontal وpaddingHorizontal تضبطان الجانبين معًا؛
  • يُحاذى النص ذو textAlign: "auto" إلى اليمين، وتنعكس عناصر التحكم الأصلية.
view()
    .flex_row()
    .gap_2()
    .child(view().ms_4().rounded_ss(px(12.0)).size(px(44.0)).bg(rgb(0x54aeff)))

let rtl = signal(false);
view()
    .direction(move || if rtl.get() { LayoutDirection::Rtl } else { LayoutDirection::Ltr })
    .on_press(move |_: &()| rtl.set(!rtl.get()))
    .child(text(move || if rtl.get() { "Right to left" } else { "Left to right" }))

تُطبَّق التغييرات في الاتجاه، سواء في اللغة أو في View معيّن، فورًا ودون إعادة تشغيل. كما يُبقي النمط direction View واحدًا وأبناءه في اتجاه خاص بهم، مثلًا رقم هاتف من اليسار إلى اليمين. على Android، تحتاج التطبيقات التي تضمّن NSH إلى android:supportsRtl="true" في ملف manifest الخاص بها كي تنعكس عناصر التحكم الأصلية.

#ذات صلة