NoWayOS
EN
GitHub
MenuClose

Build

Your first screen

Write a screen in TSX, check and convert it, see it in QEMU and on a Raspberry Pi 5, then drive it from Rust: its actions, fields, slots and groups.

Write a screen as you would in React. The Screen Compiler checks it against its subset and turns it into Rust once, at build time, on your machine. What reaches the device is that Rust, built into the system image: no JavaScript, no browser, no interpreter. This guide takes one screen from a TSX file through QEMU to a Raspberry Pi 5, then shows how an application drives a screen. Screens from React lists everything that converts.

Set up

Clone NoWayOS, the Screen Compiler and the Showcase side by side, and build the dev container once. Every make target runs inside it, so nothing else is installed on your machine; Getting started says what the container needs.

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

The Showcase is the demonstration a Pi boots to. Its React gallery shows every component the Screen Compiler converts, a screen at a time, and your first screen joins it.

Write a screen

Save this as 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>
  );
}

Each callback prop becomes an action, numbered from 1 in the order the screen uses them. Every gallery screen begins with the same bar, Home, Back and Next, so the Showcase handles those three alike on every screen; onWave is this screen's own, and here nothing handles it.

Check it

cd screen-compiler
make run ARGS="check ../showcase/showcase/showcase-app/tsx/gallery-hello.tsx"

A screen that converts gets one line back:

../showcase/showcase/showcase-app/tsx/gallery-hello.tsx: supported (1 screen, 10 elements, 0 design tokens, 4 actions)

Anything outside the subset is refused where it is written, with what to write instead, and nothing is converted. Give the column padding="1rem" and the button an onClick that logs, and the check says:

../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

converts every one of the Showcase's screens with the Screen Compiler beside it, writing the Rust to showcase/showcase-app/src/generated/: gallery-hello.rs, and a function that builds it, generated::build_gallery_hello. Then, in showcase/showcase-app/src/lib.rs, add your screen to the end of the gallery:

pub const GALLERY: [(&str, generated::Build); 12] = [
    // ... the gallery's own eleven screens ...
    ("gallery-hello", generated::build_gallery_hello),
];

and to the list just below it of screens whose bar is checked: gallery_hello::gallery_hello_screen, with gallery_hello in that list's use. The check runs as the Showcase compiles, so a bar out of order stops the build.

See it in QEMU

make qemu-test SYSTEM=showcase-gallery

plays the gallery's scripted round and matches each of its own screens, pixel for pixel, with its approved image: the rest of the gallery still draws as approved. To see yours:

make qemu SYSTEM=showcase-gallery VNC=1

QEMU's screen is published over VNC on your machine's localhost:5900, with a password printed for that run only; on macOS, open vnc://localhost:5900. What you type in the terminal reaches the Showcase: Down, Right and Enter open the React gallery, then Shift-Tab and Enter press Back, which goes round from the first screen to the last, yours. Quit QEMU with Ctrl-A, then x.

See it on the Pi

Set the board up once, as Running on a Raspberry Pi 5 shows: its card, the Debug Probe, a monitor attached before power-on and a USB mouse. Then, from showcase/, on your machine rather than in the container:

make pi-show PI_DEVICE=/dev/cu.usbmodemXXXX SYSTEM=showcase

This builds the Showcase with your screen in it, sends it to the board through the probe and leaves it running until the board is power-cycled. Click React gallery, then Back: your screen, on the Pi's HDMI monitor.

Drive a screen from Rust

A screen tells the application what happened, and the application tells the screen what to show, in four ways: actions, fields, slots and groups. The Screen Compiler's fixture fixtures/visit-booking.tsx uses all four:

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>

Its Rust names each one, numbered from 1 in the order the screen writes them:

In the TSX In the Rust The application
A callback prop, onBook An action, actions::BOOK is told when the button is pressed
A control with an id, id="name" A field, fields::NAME reads what was entered
A prop written as text, {props.phoneError} A slot, slots::PHONE_ERROR sets the text
A prop that decides what shows, {props.phoneError && …} A group, groups::PHONE_ERROR shows or hides it

The application builds the screen into a NoWayOS UI tree, sets its slots and groups, and hands the tree each input event. A tap on Book comes back as Book's action, and the application reads the fields then:

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,
    _ => {}
}

This is the code of a test in the Screen Compiler's repository, contract-check/tests/driving.rs, which builds the converted screen on NoWayOS's own UI core and taps Book in its make ci. On a device the events come from NoWayOS's input broker, and the tree is drawn by its renderer. A comparison such as props.step === "review" becomes a switch the same way, switches::STEP, which the application sets with set_case.

Where next

Screens from React has every component, prop and style property the Screen Compiler takes. To bring an existing React application across, start with Porting from React and its coverage report.