البناء
شاشتك الأولى
اكتب شاشة بـ TSX، وافحصها وحوّلها، وشاهدها في QEMU وعلى Raspberry Pi 5، ثم تحكّم فيها من Rust: إجراءاتها وحقولها وفتحاتها ومجموعاتها.
اكتب الشاشة كما تكتبها في React. يفحصها Screen Compiler مقابل مجموعته الجزئية ويحوّلها إلى Rust مرة واحدة، وقت البناء، على جهازك. وما يصل إلى الجهاز هو ذلك الـ Rust، مبنيًّا داخل صورة النظام: لا JavaScript ولا متصفح ولا مفسِّر. يأخذ هذا الدليل شاشة واحدة من ملف TSX عبر QEMU إلى Raspberry Pi 5، ثم يبيّن كيف يتحكّم التطبيق في الشاشة. وكل ما يتحوّل مذكور في شاشات من React.
الإعداد
استنسخ NoWayOS وScreen Compiler وShowcase جنبًا إلى جنب، وابنِ حاوية التطوير مرة واحدة. يعمل كل هدف من أهداف make داخلها، فلا يُثبَّت شيء آخر على جهازك؛ ويذكر البدء ما تحتاج إليه الحاوية.
mkdir nowayos-work && cd nowayos-work
git clone https://github.com/NoWayOS/nowayos.git
git clone https://github.com/NoWayOS/screen-compiler.git
git clone https://github.com/NoWayOS/showcase.git
# the NoWayOS the Showcase builds on
git -C nowayos checkout $(cat showcase/nowayos.rev)
make -C nowayos container
Showcase هو العرض التوضيحي الذي يُقلع إليه الـ Pi. ويعرض React gallery فيه كل مكوّن يحوّله Screen Compiler، شاشةً تلو الأخرى، وإليه تنضم شاشتك الأولى.
اكتب شاشة
احفظ ما يلي باسم showcase/showcase/showcase-app/tsx/gallery-hello.tsx:
import { Button, Column, HeaderBar, Screen, Spacer, Text } from "nowayos:kit";
type HelloProps = {
onHome: () => void;
onBack: () => void;
onNext: () => void;
onWave: () => void;
};
export function GalleryHelloScreen(props: HelloProps) {
return (
<Screen>
<Column gap={12} padding={16}>
<HeaderBar gap={8} padding={8}>
<Button variant="secondary" onPress={props.onHome}>Home</Button>
<Text role="heading">Hello</Text>
<Spacer />
<Button variant="secondary" onPress={props.onBack}>Back</Button>
<Button onPress={props.onNext}>Next</Button>
</HeaderBar>
<Text role="title">Hello from TSX</Text>
<Text>Written in React's syntax, compiled to Rust at build time.</Text>
<Button onClick={() => props.onWave()}>Wave</Button>
</Column>
</Screen>
);
}
تصبح كل خاصية استدعاء راجع (callback prop) إجراءً، مرقّمًا بدءًا من 1 بالترتيب الذي تستعملها به الشاشة. وتبدأ كل شاشة في المعرض بالشريط نفسه، Home وBack وNext، فيعامل Showcase هذه الثلاثة المعاملة نفسها في كل شاشة؛ أما onWave فخاص بهذه الشاشة، ولا شيء يعالجه هنا.
افحصها
cd screen-compiler
make run ARGS="check ../showcase/showcase/showcase-app/tsx/gallery-hello.tsx"
الشاشة التي تتحوّل يعود إليها سطر واحد:
../showcase/showcase/showcase-app/tsx/gallery-hello.tsx: supported (1 screen, 10 elements, 0 design tokens, 4 actions)
وكل ما يقع خارج المجموعة الجزئية يُرفض في الموضع الذي كُتب فيه، مع ما يُكتب بدلًا منه، ولا يتحوّل شيء. أعطِ العمود padding="1rem" والزرَّ onClick يكتب في السجل، فيقول الفحص:
../showcase/showcase/showcase-app/tsx/gallery-hello.tsx: not supported
../showcase/showcase/showcase-app/tsx/gallery-hello.tsx:13:32: error[unsupported]: `padding` in `rem` is not supported: lengths are layout units, written in px
help: write padding="16px", padding={16} or a design token
../showcase/showcase/showcase-app/tsx/gallery-hello.tsx:23:26: error[unsupported]: a function here may only call a callback prop, with no arguments
help: pass a callback prop of the component: onClick={onContinue}, or onClick={() => props.onContinue()}
../showcase/showcase/showcase-app/tsx/gallery-hello.tsx: 2 errors
أضفها إلى المعرض
cd ../showcase
make screens-bless
يحوّل هذا الأمر كل شاشات Showcase بـ Screen Compiler المجاور، ويكتب الـ Rust في showcase/showcase-app/src/generated/: الملف gallery-hello.rs، ودالة تبنيها، generated::build_gallery_hello. ثم أضف شاشتك في showcase/showcase-app/src/lib.rs إلى آخر المعرض:
pub const GALLERY: [(&str, generated::Build); 12] = [
// ... the gallery's own eleven screens ...
("gallery-hello", generated::build_gallery_hello),
];
وإلى القائمة التي تليه مباشرة، قائمة الشاشات التي يُفحص شريطها: gallery_hello::gallery_hello_screen، مع gallery_hello في use تلك القائمة. يجري الفحص أثناء ترجمة Showcase، فالشريط الذي اختلّ ترتيبه يوقف البناء.
شاهدها في QEMU
make qemu-test SYSTEM=showcase-gallery
يؤدّي هذا الأمر جولة المعرض المكتوبة سلفًا، ويطابق كل شاشة من شاشاته، بكسلًا ببكسل، مع صورتها المعتمدة: فبقية المعرض ما زالت تُرسم كما اعتُمدت. ولترى شاشتك:
make qemu SYSTEM=showcase-gallery VNC=1
تُنشر شاشة QEMU عبر VNC على localhost:5900 في جهازك، مع كلمة مرور تُطبع لذلك التشغيل وحده؛ وعلى macOS، افتح vnc://localhost:5900. وما تكتبه في الطرفية يصل إلى Showcase: السهم للأسفل والسهم لليمين وEnter تفتح React gallery، ثم Shift-Tab وEnter تضغطان Back، الذي يدور من الشاشة الأولى إلى الأخيرة، أي شاشتك. واخرج من QEMU بالضغط على Ctrl-A، ثم x.
شاهدها على الـ Pi
جهّز اللوحة مرة واحدة كما يبيّن التشغيل على Raspberry Pi 5: بطاقتها، وDebug Probe، وشاشة عرض موصولة قبل التشغيل، وفأرة USB. ثم شغّل من showcase/، على جهازك لا داخل الحاوية:
make pi-show PI_DEVICE=/dev/cu.usbmodemXXXX SYSTEM=showcase
يبني هذا الأمر Showcase وفيه شاشتك، ويرسله إلى اللوحة عبر المِسبار، ويتركه يعمل حتى تُطفأ اللوحة ثم تُشغَّل من جديد. انقر React gallery، ثم Back: ها هي شاشتك على شاشة HDMI الموصولة بالـ Pi.
تحكّم في الشاشة من Rust
تخبر الشاشةُ التطبيقَ بما حدث، ويخبر التطبيقُ الشاشةَ بما تعرضه، بأربع طرق: الإجراءات والحقول والفتحات والمجموعات. وتستعمل الأداة الاختبارية fixtures/visit-booking.tsx في Screen Compiler الطرق الأربع كلها:
type VisitBookingProps = {
phoneError: string;
onBack: () => void;
onBook: () => void;
};
// ...
<TextInput id="name" placeholder="Your name" maxLength={NAME_LEN} />
{props.phoneError && <Text>{props.phoneError}</Text>}
<RadioGroup id="when" options={["Morning", "Afternoon", "Evening"]} />
<Checkbox id="remind" label="Remind me the day before" />
<Button id="book" onPress={props.onBook}>Book</Button>
ويسمّي الـ Rust الناتج عنها كل واحد منها، مرقّمًا بدءًا من 1 بالترتيب الذي تكتبها به الشاشة:
| في TSX | في Rust | التطبيق |
|---|---|---|
خاصية استدعاء راجع، onBook |
إجراء، actions::BOOK |
يُبلَّغ حين يُضغط الزر |
عنصر تحكّم له id، id="name" |
حقل، fields::NAME |
يقرأ ما أُدخل |
خاصية مكتوبة نصًّا، {props.phoneError} |
فتحة، slots::PHONE_ERROR |
يضبط النص |
خاصية تقرّر ما يظهر، {props.phoneError && …} |
مجموعة، groups::PHONE_ERROR |
يُظهرها أو يخفيها |
يبني التطبيق الشاشة في شجرة واجهة من NoWayOS، ويضبط فتحاتها ومجموعاتها، ويسلّم الشجرة كل حدث إدخال. فتعود النقرة على Book بوصفها إجراء Book، وعندها يقرأ التطبيق الحقول:
let mut tree = Tree::<64>::new();
let screen = visit_booking::build(&mut tree)?;
tree.set_root(screen.root)?;
// A slot is text the application sets; a group is content it shows or hides.
tree.set_slot(slots::PHONE_ERROR, "Digits only, please")?;
tree.set_visible(groups::PHONE_ERROR, true)?;
// Every input event goes to the tree; a tap on Book comes back as its action.
match tree.handle(event) {
Some(UiEvent::Action(actions::BOOK)) => {
let name = tree.value(fields::NAME).unwrap_or("");
let when = tree.selected(fields::WHEN);
let remind = tree.checked(fields::REMIND).unwrap_or(false);
booked = Some((name.to_owned(), when, remind));
}
Some(UiEvent::Action(actions::BACK)) => back = true,
_ => {}
}
هذه شيفرة اختبار في مستودع Screen Compiler، هو contract-check/tests/driving.rs، يبني الشاشة المحوّلة على نواة الواجهة في NoWayOS نفسها وينقر Book ضمن make ci الخاص به. وعلى الجهاز تأتي الأحداث من وسيط الإدخال في NoWayOS، ويرسم الشجرةَ مُصيِّرُه. والمقارنة مثل props.step === "review" تصبح بالطريقة نفسها مفتاحًا، switches::STEP، يضبطه التطبيق بـ set_case.
ماذا بعد
في شاشات من React كل مكوّن وخاصية وخاصية نمط يقبلها Screen Compiler. ولتنقل تطبيق React قائمًا، ابدأ بـ النقل من React وتقرير التغطية فيه.