构建
您的第一个屏幕
用 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>
);
}
每个回调 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
这条命令用旁边的 Screen Compiler 转换 Showcase 的每一个屏幕,并把 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,并在该列表的 use 中加入 gallery_hello。这项检查在 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,它会从第一个屏幕绕到最后一个,也就是您的屏幕。按 Ctrl-A,再按 x,即可退出 QEMU。
在 Pi 上查看
按在 Raspberry Pi 5 上运行所示,把开发板准备一次:它的存储卡、Debug Probe、通电前接好的显示器,以及一个 USB 鼠标。然后在 showcase/ 中,在您的机器上而不是容器里运行:
make pi-show PI_DEVICE=/dev/cu.usbmodemXXXX SYSTEM=showcase
这条命令构建包含您屏幕的 Showcase,通过调试探针把它发送到开发板,并让它一直运行,直到开发板断电重启。点击 React gallery,再点击 Back:您的屏幕就出现在 Pi 的 HDMI 显示器上。
从 Rust 驱动屏幕
屏幕告诉应用发生了什么,应用告诉屏幕显示什么,方式有四种:动作、字段、插槽和组。Screen Compiler 的测试样例 fixtures/visit-booking.tsx 四种都用到了:
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 中 | 应用 |
|---|---|---|
一个回调 prop,onBook |
一个动作,actions::BOOK |
在按钮被按下时得到通知 |
一个带 id 的控件,id="name" |
一个字段,fields::NAME |
读取输入的内容 |
一个写成文本的 prop,{props.phoneError} |
一个插槽,slots::PHONE_ERROR |
设置文本 |
一个决定显示什么的 prop,{props.phoneError && …} |
一个组,groups::PHONE_ERROR |
显示或隐藏它 |
应用把屏幕构建到 NoWayOS 的 UI 树中,设置它的插槽和组,并把每个输入事件交给这棵树。点按 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 自己的 UI 核心上构建转换后的屏幕,并在 make ci 中点按 Book。在设备上,事件来自 NoWayOS 的输入代理,树由它的渲染器绘制。像 props.step === "review" 这样的比较也以同样方式变成一个开关 switches::STEP,应用用 set_case 设置它。
接下来
Screen Compiler 接受的每个组件、prop 和样式属性都在用 React 制作屏幕中。要迁移一个现有的 React 应用,请从从 React 移植及其覆盖率报告开始。