Theme
Last updated: 2026-08-25
An app can add its own items to the glasses contextual menu - the overlay the OS raises on tap then long press (SDK 0.0.14+, Even App 2.2.9+). Each item you declare is an action: the user selects it, your app gets one event, and the menu closes.
The menu is declared with the page, not fetched on demand. You attach a menuObject to createStartUpPageContainer or rebuildPageContainer; the OS holds it and renders it without waking your WebView.
Slots
The glasses OS owns the menu frame. Your items land between permanent system slots:
| Slot | Owner | Notes |
|---|---|---|
| Display off (top) | System | Always present, not reachable from the SDK |
| Brightness | System | Always present. Handled end to end by the OS - your app is never notified. Global display brightness, unrelated to the per-container textColor levels |
| Your action items | Your app | Declared via menuObject, up to 10 |
| Close [app name] (bottom) | System | Always present, not reachable from the SDK. Renders your app's name, so the row reads Close Timer rather than a bare Close |
Declare nothing and the user still gets the system items - the same default every unadapted app shows. Your items exist only for the life of the page that declared them.
Don't count screen rows. The system set can grow between firmware releases and none of it is visible to the SDK, so treat the menu as your items wrapped in system items you don't control. You hand over a list; you never address a slot.
Declaring a menu
typescript
import { waitForEvenAppBridge } from '@evenrealities/even_hub_sdk'
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer({
containerTotalNum: 1,
textObject: [{
xPosition: 0,
yPosition: 0,
width: 576,
height: 288,
containerID: 1,
containerName: 'main',
content: 'Timer running',
isEventCapture: 1,
}],
menuObject: {
menuItems: [
{ itemName: 'Restart', itemID: 1 },
{ itemName: 'Recenter', itemID: 2 },
],
},
})menuItems[] fields
| Field | Type | Required | Notes |
|---|---|---|---|
itemName | string | Yes | Label the OS renders. Max 32 UTF-8 bytes - not 32 characters |
itemID | number | Yes | Non-zero uint32, unique across the menu. Comes back on the click event |
itemID cannot be 0. Zero is reserved by the protocol, so start your IDs at 1.
There is no ordering field. Items render in payload order - MenuItemProperty carries itemName and itemID and nothing else, and its toJson() drops any other property before the payload leaves the SDK. Reorder the array to reorder the menu.
Handling a selection
The selected item arrives on onEvenHubEvent as menuItemClickEvent, carrying only the itemID you assigned:
typescript
const handlers: Record<number, () => void> = {
1: () => restartTimer(),
2: () => recenterView(),
}
const unsubscribe = bridge.onEvenHubEvent(event => {
const itemID = event.menuItemClickEvent?.itemID
if (itemID === undefined) return
handlers[itemID]?.()
})
// unsubscribe()Menu clicks do not route through isEventCapture. menuItemClickEvent is its own top-level field on the event, independent of the list/text container routing, so it arrives whichever container is capturing. Handle it on the same onEvenHubEvent subscription you already use for list, text, and system events.
The menu is a foreground overlay
The OS draws the menu on top of your page, and it tells your app about it through the ordinary foreground events. Opening the menu delivers FOREGROUND_ENTER_EVENT (4); dismissing it delivers FOREGROUND_EXIT_EVENT (5). Selecting an item is the whole sequence:
FOREGROUND_ENTER_EVENT -> menuItemClickEvent -> FOREGROUND_EXIT_EVENTFOREGROUND_EXIT_EVENT here means the overlay went away. It does not mean your app was torn down - your page stays mounted and owns the screen underneath the whole time.
That distinction decides how you write the handler. If foreground exit is where you stop timers, drop subscriptions, or clear in-progress state, that work now runs every time the user opens the menu. Make the handler idempotent, or separate "the overlay closed" from "the user left" before doing anything destructive.
Fire-and-forget only
Every item is an action. Selecting one sends a single event and closes the menu - the glasses do not hold or re-render item state, and there is no acknowledgement path back from your app to the menu.
So an item that reads Status: high will still read Status: high the next time the user opens the menu, even if your handler changed the value. If the label has to track state, re-declare the menu:
typescript
async function setQuality(next: 'high' | 'low') {
quality = next
await bridge.rebuildPageContainer({
containerTotalNum: 1,
textObject: [/* ... */],
menuObject: {
menuItems: [
{ itemName: `Quality: ${quality}`, itemID: 1 },
],
},
})
}Design the labels as verbs (Restart, Skip, Mute) rather than as state readouts, and this stops mattering.
Updating and clearing
menuObject is replaced wholesale, never merged:
| Call | Effect on the menu |
|---|---|
rebuildPageContainer with menuObject | Replaces the whole menu with what you sent |
rebuildPageContainer without menuObject | Clears your items; the system items remain |
Omitting menuObject on a rebuild is significant, not neutral. A rebuild that drops it because the layout changed will also drop the menu - carry it forward explicitly on every rebuild that should keep it.
typescript
// Clears the custom menu, restores default registration
await bridge.rebuildPageContainer({
containerTotalNum: 1,
textObject: [/* ... */],
})Validation
The SDK checks the menu before it reaches the glasses. On a violation it logs an EvenHubPageContainerValidationErrorCode and the call fails locally - createStartUpPageContainer returns StartUpPageCreateResult.invalid, rebuildPageContainer returns false. Nothing partial reaches the firmware.
| Code | Cause |
|---|---|
TOO_MANY_MENU_ITEMS | More than 10 items in menuItems |
INVALID_MENU_ITEM_ID | itemID is 0, negative, non-integral, or outside uint32 |
DUPLICATE_MENU_ITEM_ID | The same itemID used twice |
INVALID_MENU_ITEM_NAME | itemName exceeds 32 UTF-8 bytes |
Validation runs client-side, so a bad menu fails in development rather than shipping as a silently missing menu.
Version gate
The contextual menu needs SDK 0.0.14 and Even App 2.2.9. On an older app the declaration is a silent no-op - the page still renders, the user still gets the system items, your items just never appear. Building against SDK 0.0.14 makes the CLI stamp the 2.2.9 floor at pack time, so those users are blocked at open instead. See Auto-deriving min_app_version.
Testing it
Simulator 0.9.0+ draws the menu, so hardware is not needed to see whether yours is right. In the window, tap then long press raises it; over the automation API, POST /api/input with {"action":"context_menu"} toggles it, up / down move focus, and click fires your menuItemClickEvent.
The simulator honours the rebuild rules above - carry menuObject forward and the menu comes back identical, omit it and your items are gone on the next open. One wrinkle it does not share with the contract: a rebuild underneath an already open menu does not dismiss the overlay, so what you see may outlive the declaration behind it. What the simulator can't confirm is which system slots the OS puts alongside your items on a given firmware build.
Design notes
- 32 UTF-8 bytes, not characters. ASCII gets 32; CJK is 3 bytes per glyph, so a Chinese label is capped near 10.
- Keep labels short. The OS renders one line per slot and doesn't wrap. Under ~16 ASCII characters reads cleanly.
- 10 is the ceiling, not the target. A menu is a shortcut list. Past five or six items, users scroll further than they would have tapped.
- Don't duplicate the page. The menu is for what the current screen can't reach, not a second copy of its controls.
- Closing lives in the system slot. Don't add your own exit item - the root-page double-tap contract in App Submission is unchanged and still required.