Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Two-column list for Even Realities G2

A minimal, runnable Even Hub app that renders a scrollable two-column list on the G2 display — checkboxes, strikethrough on checked rows, a live "N left" tally, and cursor navigation that flows down column 1 and then down column 2.

The list rendered on the glasses

This is the list screen extracted out of Fushopping List (a shopping list app part of fuscripts.com with a companion app for the G2 glasses), stripped of its auth and networking so the layout technique is all that's left. Mock data, no accounts, no backend — clone it and run it.

swipe        move the cursor
press        check the item off
double-press exit

The technique

There is no column widget, no grid layout, and no flexbox on the G2. A TextContainerProperty is a positioned box that renders a string, splitting it on \n at a fixed 27px line height. That's the entire toolbox.

So a two-column list is just two text containers side by side, each holding nine lines joined by \n:

        ┌──────────────────────┬──────────────────────┐
        │ [ ] Milk             │ [ ] Tomato sauce     │   container "body0"    container "body1"
        │ [ ] Eggs             │ [ ] Onions           │   x = 34               x = 322
        │ [ ] Bread            │ [ ] Garlic           │   w = 244              w = 244
        │ … 9 lines total      │ … 9 lines total      │   h = 9 × 27 = 243
        └──────────────────────┴──────────────────────┘

Four things make it work.

1. Items flow column-major, so the page is a flat slice

Items go down column 0 and then down column 1 — not left-to-right. That one choice is what keeps the code simple: the visible page is just

const slice = items.slice(top, top + 18)   // 2 cols × 9 rows

and the renderer chops that slice into columns. Scrolling moves top by one item, so when the cursor walks off the bottom of column 1 the whole grid pulls up by one row and the item that was at the top of column 0 disappears. The state layer never knows columns exist (src/state.ts); the render layer never knows about scrolling (src/render.ts).

Mapping the cursor to a cell is then two lines:

const visible = state.index - state.top
const col = Math.floor(visible / ITEMS_ROWS)
const row = visible % ITEMS_ROWS

2. One container per column, not per row

Tempting to emit one container per item — 18 of them — and position each. Don't. The whole page is re-sent over BLE on every single gesture, and the frame cost scales with container count. Two containers instead of eighteen is the difference between a list that responds instantly and one that visibly crawls.

3. An invisible capture layer stops scroll-bounce

This one costs an afternoon if you don't know it. If a visible container is the one with isEventCapture: 1, the firmware scrolls that container itself on a temple swipe — so the text slides, springs back under your own re-render, and every gesture flickers.

The fix: exactly one full-screen, transparent container (content is a single space) is the only isEventCapture: 1 layer, and every visible container is isEventCapture: 0. Swipes land on the invisible layer, which has nothing to scroll, and you move the cursor yourself.

new TextContainerProperty({
  xPosition: 0, yPosition: 0, width: 576, height: 288,
  containerID: 1, containerName: 'evt',
  content: ' ',
  isEventCapture: 1,
})

4. Strikethrough is a second container stacked on top

The G2 font has no combining strike, and the text container API exposes no per-run styling or brightness — a checked row can't be dimmed or struck directly. So a second container sits exactly over the text column, offset past the [x] prefix, holding one run of (U+2500, which connects seam to seam) per checked row and an empty line for every unchecked one:

const dashes = Math.floor(getTextWidth(name) / getTextWidth('─'))

Floor the division — a run wider than its text wraps the line and pushes the whole column out of alignment.

Measuring text

Everything above depends on knowing how wide a string renders. @evenrealities/pretext's getTextWidth() measures against the same font the firmware uses, which is what makes truncation and centering land where you expect:

  • truncate every line before sending it — an overflowing line wraps to a second row and silently pushes the rest of the column down
  • center by padding with spaces (' '.repeat(...)), because there is no text-align

Sizing note

9 rows × 27px = 243px, and the canvas is 288px tall. That leaves room for the header strip or a bottom gesture hint, not both. This demo spends it on a 9th row and documents the gestures on the phone screen instead. The header also only renders while top === 0, so it scrolls away with the list rather than sitting there as a fixed bar — and because the grid geometry doesn't change either way, crossing that boundary never reflows the items.


Run it

npm install
npm run dev        # vite on :5173

In the simulator (no glasses needed):

npm run sim        # points the simulator at localhost:5173

On real glasses — sideload the dev server over your LAN:

npm run qr         # prints a QR for http://<your-lan-ip>:5173

Scan it with the Even app (Developer Mode on). The qr script resolves your LAN IP with macOS's ipconfig; on other platforms run npx evenhub qr --url http://<your-ip>:5173 directly. Your phone and computer have to be on the same network.

Package it into an installable .ehpk:

npm run pack       # → two-column-list.ehpk

Change package_id in app.json before you publish anything — it's currently com.example.twocolumnlist.

Layout

src/
  main.ts     bridge lifecycle, gesture routing, frame sending
  render.ts   ← the interesting file: state → text containers
  state.ts    cursor movement + the sticky scroll window
  demo.ts     mock groceries
  types.ts

Two more things in main.ts worth stealing for a real app, both learned the hard way in Fushopping List:

  • every bridge call goes through one serialized queue with a timeout — a flaky BLE hop otherwise hangs for ~30 seconds with the glasses frozen mid-frame
  • identical frames are never re-sent — rebuilding the same page makes the display visibly redraw, which reads as a flicker on every no-op gesture

License

MIT — do whatever you like with it.

About

A two-column scrollable list for Even Realities G2 smart glasses — the layout technique from Fushopping List, extracted into a runnable demo

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages