OpenBikeComputer
On this page

The UI system

The UI is a no_std, allocation-free system for a 240 × 320 display and four buttons. It draws immediately: there is no retained widget tree, and a screen writes the frame it wants.

The screen drawings below are schematics. They explain behavior. They are not pixel captures.

Screen model

Each Screen variant owns its state by value. One table generates the enum, dispatch, and capabilities. Shared behavior reads those capabilities rather than matching variants:

CapabilityWhat it decides
kindBase, Overlay or Settings. The overlay and settings behaviors hang off it.
baseMap, LiveRiding or Chrome. It gates map reads, live-data repaint, and the Bluetooth indicator.
readerWhen the screen needs the streamed map at draw: never, always, or for articles, a photo, or POI data.
render_keyWhich set of facts a repaint of this screen depends on.
recessA drawer draws this screen again one shade down, instead of leaving it standing.
idle_exemptThe idle-return timeout must never take this screen away.
ride_viewA deliberate ride view: the idle timeout leaves it while a ride is tracked.
browse_exemptA deliberate browse view: the idle timeout does not return it to Home when no ride is tracked.
blocks_chordsWhile it is on top, the device-wide drawer chords are refused.
blocks_escapeWhile it is on top, the global Back-hold escape does not leave it.
A screen is a value — no retained widget tree enum Screen Map(MapScreen) Home(HomeScreen) Statistics(…) RideControl(…) Menu(…) ContextDrawer(…) RouteMenu(…) RouteOverview(…) RouteSwap(…) screen/map.rs struct MapScreen { … } typed state — owned, inline, no alloc fn handle(g, &mut Ctx) → Transition logic — react to a gesture, ask to navigate fn draw(target, &Render) pixels — read state, paint the panel dispatched by match — static, zero-alloc one row per screen, with its capabilities
Each screen owns typed state. The Screen enum provides static dispatch without heap allocation.

A screen handles one gesture, returns a transition, and draws the current frame.

Two halves of the world, handed to two methods handle(g, &mut Ctx) mutable — change the world state   — camera · zoom · pan activity — ride mode · UI requests navigator — route · match · guidance recorder — ride totals · sensors settings — units · clock · intervals draw(target, &Render) read-only — paint the world reader · renderer — draw the map state · guidance · route caches · breadcrumb w · h  — panel size hold_progress — the confirm ring
Input handling uses mutable Ctx. Drawing uses read-only Render data and borrowed render resources.

Input can change state; drawing receives read-only state and borrowed resources. The screen module owns the interfaces and dispatch. Host drawing generics stop at that boundary.

The screen stack holds at most ten screens, and Home is always the first.

A screen returns a navigation transition Gesture Top screen receives input handle Returns Transition Apply transition Update the screen stack Stack example Up ahead · top Map Home · root Push / Pop Add / remove a top screen Replace Change the top screen Root(screen) Keep Home, then add screen Home Keep only Home None Keep the current stack
A screen returns a transition, and screen::apply runs it. Cards land on the stack without it.
TransitionStack operation
NoneKeep the stack.
Push(screen)Add a top screen.
PopRemove the top screen, except Home.
Replace(screen)Replace the top screen.
OverRoot(screen)Keep Home and the view under it, and add one screen.
Root(screen)Keep Home and add one screen.
HomeRemove all screens above Home.

Stack changes cancel incomplete holds. Each screen defines Back: leave an editor, cancel work, change riding view, or pop.

Input

Four buttons → one recognizer → gestures and chords left flank ▲ Up ▼ Down steps · auto-repeat right flank Select Back Gestures raw events + ms clock shared: sim = MCU Step(n) — up / down Press — short select Hold — long select Back — short back BackHold — long back Two-button chords · within 100 msUp + Select: quick drawerDown + Back: contextual drawer
The recognizer combines button state and elapsed time. A chord consumes both presses and does not also emit their individual gestures.
GestureSource
Step(n)Up or Down step
PressSelect release within 200 ms
HoldSelect held for 500 ms
BackBack release within 200 ms
BackHoldBack held for 500 ms

Release between the tap and hold thresholds cancels the hold. A hold fires at its threshold, so the rider feels when it commits.

The application handles BackHold above the stack. It closes the drawer and opens or reuses the main menu. First-use setup, blocking cards, the recovered-ride card, and confirmed shutdown refuse it until the rider finishes them.

Chords

Buttons pressed within 100 ms form a chord. It consumes both individual gestures and stays latched until both buttons are released.

ChordMeaning
Up + Select, released before 500 msOpen or close the universal quick drawer
Up + Select, held for 500 msOpen Ride Assistant
Down + BackOpen or close the current screen's contextual drawer
Up + Down, held for 2 secondsOpen Help
Select + BackReserved

Reserved chords consume both presses. The first Up or Down step waits for the chord window; an earlier release steps immediately to keep taps responsive.

Drawers

Drawers are sheets over the current screen. Opening one replaces the other.

The universal quick drawer descends with brightness, Bluetooth, settings, and power. Brightness appears only on platforms with a controllable backlight.

The contextual drawer rises with the screen's secondary actions. A shared drawer handles input and drawing from a static row table. Without a table, no sheet opens.

Riding views list Up ahead, Detour, POIs, and Routes in that order. Unavailable rows are recessed and inactive. An action replaces the sheet; Back returns to the riding view.

A value row opens an editor with a notch per choice and a committed-value tick. Bike type has four choices. A switch row flips in place.

Screen-specific settings belong in that screen's drawer. A build check rejects duplicate settings bindings, except brightness, Bluetooth, and bike type.

The screen under a drawer is frozen. Only the drawer's facts trigger repaint. Menus dim to separate the sheet from its base. Maps remain undimmed to avoid an expensive redraw.

An incoming card closes the drawer. Dismissing it returns to the underlying screen.

What the map drawer controls

Map display holds switches for the clock, the scale bar, and the contour layer, and a Map icons menu with switches for peaks, landmarks, and the service categories. Icons stay upright at their source coordinates as the map rotates, and a group appears only at scales where its icons help instead of crowd. Placement takes the nearest unobstructed icon from each enabled group in turn, so one dense category cannot take the screen from the others.

Settlement names have no switch. Each class appears within a scale band, when the place roughly fits the screen. Names disappear when too small or large to help, and avoid each other and visible chrome.

Detour

Detour leaves the route and comes back to it. It needs a routing graph in the map and a matched position on the route.

Choose a rejoin · preview the cost · commit the splice ① chooser — Up/Down move the rejoin skipped stretch rejoin ±100 m steps · 600 m minimum ② preview — path and cost +434 m -47 m what it costs: distance · climbing ③ commit — use the detour an ordinary route file back cancels at any step · a failed plan suggests the one useful remedy: try a farther rejoin
The rider selects a rejoin point, reviews the result, and commits the new route.

Up and Down move the rejoin point ahead. Review compares distance and climb with the replaced stretch. Committing preserves completed geometry and inserts the detour.

Hold to confirm

Hold to confirm — the guarded-action pattern down tap · 200 ms hold · 500 ms release → Press release → nothing → Hold fires (commits, still held) the selected row, as you hold Discard 0% — idle Discard holding — release = cancel Discard ✓ 100% — committed
A guarded action runs only when the hold reaches its threshold. The live fill shows hold progress.

A destructive action requires a hold, and the row draws its progress. The action runs only when the hold reaches its threshold.

One guarded hold — the Fields footer, the Route overview + Ride detail rows FIELDS SPEED24.3 AVG KPH17.0 KM DONE42.5 CLIMBED▲810 hold to delete bar fills on the live hold the guarded hold, two states hold to delete — normal · a completed hold deletes — hidden · item is active / recording
Delete actions use the same guarded-row contract. A hold on another row has no effect.

Delete lives on one row. A hold anywhere else deletes nothing.

Ride Assistant

Hold Up + Select to open Ride Assistant over the riding view. It closes previous pages and cancels their searches or pending detours. Back returns to the ride. Find a place, What is next, Nearby landmarks, and Easier routes use installed offline data.

Help

Open Help in Assistant, or hold Up + Down for two seconds. Help shows 112 and the GPS position in WGS84 decimal degrees and degrees, minutes and seconds. The fix age and status distinguish a current position from the last known position. Coordinates work without a map. A map can add a nearby settlement and a calibrated height.

Help stays open until the rider leaves. Select starts or stops the distress signal. The signal plays at Loud, even with Sound off. Leaving Help stops it. It sounds six times in one minute, then stays quiet for one minute, and repeats. Use a phone to call; the device does not contact emergency services.

Find a place

Find alternates nearby places with places along the next route stretch. It calculates outward and return legs, then composes the full route when a result opens. The drawer sets the result count; the default is four.

Places known to be closed now are hidden, which the rider can turn off for every category at once. A place with unknown hours stays in the list and is never labelled open. Opening hours are information: they never block a preview, and the device reports the status now, not a guess at the status on arrival.

With a route, review compares the visit, return and remaining journey against the original remainder. Without a route, the place is a direct destination. Acceptance starts recording if needed; browsing changes nothing.

A place is routable only where the map gives it a mapped approach. A straight-line distance never promises a rideable connection.

What is next

The line shows the remaining route, with places ahead below the rider. Settlements group nearby services; waypoints and the route end stay visible through filters. Larger spaces mark long distances between stops. Select opens details. Hold refreshes the list. Service records follow the map format.

Nearby landmarks

Landmarks keeps a stable page of nearby sites. Select opens text and an available photo. Down + Back opens sources, so credits travel with content. Closed sites stay readable and visitable from outside.

Easier routes

Easier routes runs bounded searches for less climbing, smoother surfaces, and shorter distance under the rider's bike profile. Results must improve their goal within an added-cost bound; discovery is not exhaustive.

The comparison camera stays fixed. Select opens a current-and-new table. Use this route accepts it and keeps recording. Shorter distance does not promise shorter time; the device has no arrival model.

POI browser

Menu → categories → pages of nearby places Menu POIs station press POIS Water Campsite Lodging Resupply Pharmacy Bike shop press Water · 1/8 Stadtbrunnen 210m Spring 410m Brunnen Nord 820m Drinking water 1km name · arrow · distance
The POI browser has seven categories. Each page holds at most eight nearby places.

The POI menu lists the service categories, and a category returns pages of nearby places.

The row, and where the arrow's "up" comes from Stadtbrunnen name (or subtype label if unnamed) bearing 210m distance the arrow's heading reference moving GPS course over ground — the direction you're going stationary ICM-20948 compass — which way you're facing neither known arrow hidden — don't point wrong
The list keeps a fixed snapshot. Only the bearing arrow uses live position data.

The page and its order are fixed while the list is open, so a list does not reorder under a rider's thumb as the position updates. Only the bearing arrow follows the live fix.

The detail screen, and where "open now" comes from POI Stadtbaeckerei Bakery 850m Today 08:00-18:00 OPEN ▶ Preview route the hours heading — three states Today — open some hours today; ranges stacked below Closed today — has hours, but none this weekday Hours not listed — HoursRef was 0xFFFF, no badge the OPEN / CLOSED badge local clock → weekday + minute-of-day is_open? read live every frame — the one part that isn't frozen
The detail screen reads opening hours once. The bearing and distance remain live.

The detail screen reads the schedule once and calculates today's state from the local clock. A definite open or closed answer needs a trusted clock and a known local offset. Anything else is Unknown.

Settings

Pages sit under one hub, every page a list of rows in the drawers' grammar:

RowLooks likePress
Doorlabel, chevronopens a page
Valuelabel, value under it, chevronopens the editor as a sheet over the page
Switchlabel, sliderflips in place
Actionlabel, red when destructiveacts; a destructive one needs a hold
Infolabel, value under itnothing; the cursor skips it
Languagea pick list: flag, own name, a tick on the committed onecommits and returns

A changed value is written when the rider leaves the Settings subtree, not once per step.

Settings do not live on the card, so they survive a card change. The UI languages are English, German, French, and Spanish, and the build generates the translation table from four catalogs and fails on a missing or extra key.

Route cleanup

The device deletes nothing by itself. When a route upload fails because storage is full, it offers cleanup: choose an age in weeks and hold to confirm. Cleanup keeps the active route, routes newer than that age, and routes whose date is unknown, because an unknown date is not evidence that a route is old. See ride reconciliation.

Repaint policy

Rendering is on demand. A static screen with no input, data change, or timer stays idle.

The render key

Each screen declares its drawing facts. Frame work compares them before and after, then repaints changed regions. A new heart rate updates its grid and map effort band. Local state, such as a selection, requests repaint directly. Extra repaint is safe; missing repaint is a defect.

Transient feedback · composite a window over the clean frame Clean base frame Transient hold window Presented frame + Hold to confirm Hold to confirm Base pixels stay unchanged. Only the active window draws. Base + hold feedback On release or completion, present the clean window again to remove the feedback.
This illustrates hold feedback, not a retained notification screen. Cards and drawers follow their own screen-stack and repaint rules.

The overlay presenter reads the clean base frame, adds the overlay, and leaves the base frame unchanged.

Screens the companion pushes

The companion can open modal cards for pairing, route and trip updates, and warnings. The scheduler gives each type a fixed priority, and a new card never replaces a hold in progress. The passkey card cannot be dismissed before pairing ends.

Riding data

Climbs

The climb panel — gradient shown as colour CLIMB 1762 m CLIMB LEFT GRADE AVG local gradient → stripe colour < 3% 3–6 6–9 9–12 > 12% · climbs are segmented once, when the route loads · a dip is bridged, a deep col splits — plain gates on gain, average grade, and length · a finer profile, scoped to the active climb, rebuilt only when you cross into the next one
The climb view shows the current climb profile and four climb values.

The route processor supplies the climb segments and their profiles. Climb joins the riding views only while a climb is active.

Waypoints

Waypoints — diamonds, the approach chip, progress-bar ticks 20m Pass · 299 m waypoint + approach chip the Statistics progress bar shares the route's distance axis waypoint at the start the pass you are here · the fill sweeping toward the next tick is free "distance to go" · ticks are ink, never red — the bar itself tints red off-route, where a red tick would vanish · off-route the chip hides and the bar freezes — the along-route distance is meaningless once you've left the line
Map, route, and statistics views use the same route-distance axis for waypoints.

Waypoints come from the route file, and Navigator tracks the next one from the matched position. The map, route, and statistics views place them on the same route-distance axis, so a waypoint is at the same progress in all three.

Up ahead

Two spatial questions — one map, one index, two windows ① near me — a disc around the fix out of range bearing nearest 16 · by straight-line distance · route-blind ② up ahead — a corridor along the route 1.2 km 4.8 km off corridor ±300 m either side · only what is still ahead of you → the POIs browser — rows by distance, with a live bearing arrow → the Up ahead timeline — rows along the route, with distance-to-go, climb-to-go and a side
Nearby POIs use geographic distance. Up-ahead entries use distance along the route.

Nearby POIs use the distance through the air. Up ahead uses the distance along the route, which is the distance a rider actually has to ride.

The row · the source cue Fontaine du port 219m 13m 271m icon + pip name, ellipsized to fit distance-to-go climb-to-go side, past 50 m the icon says which source map POI map POI · cursor waypoint waypoint · cursor the pip is the colourblind-safe half of the pair
The Up-ahead view merges two sorted sources without copying rows.

Each corridor pass adds one row at its nearest point. Leaving and returning adds another encounter; a road bend does not.

A saved cursor applies only to its list. Changing filters selects the first row still ahead, so a previous scroll cannot skip nearer matches.

Main rider flow

Main rider paths Start a route Home Main menu Routes Overview Map Start During a ride Map Back → Statistics Statistics Back → Climb or Map Climb Active + enabled · Back → Map Select on Map Pause · Resume returns to the ride Select-hold on Map Inspect · Back returns to Map Down + Back Context drawer · Up ahead, Detour, POIs, Routes Up + Select Quick drawer · device controls Back-hold Main menu · except in blocking states Back returns to MapBack skips Climb when it is inactive or disabled.
Start opens Map on a clean ride stack. Back cycles the available riding views. Drawers and Inspect keep a direct return to the ride.

Home opens the main menu, a route selection opens its overview, and Start opens Map on a clean ride stack. During a ride, Back cycles the riding views, a press pauses, and Back-hold reaches the main menu. Map Inspect is entered with a Select hold, and Back leaves Inspect before it changes views. Idle return removes abandoned chrome: it returns to Home when there is no ride, and to Map during one.

Visual vocabulary

Each shared mechanism has one owner, one module per concept under screen/vocab/:

ModuleWhat it owns
chromeThe framed page header, the card glyphs, the Recalculating banner, and the shared text and stroke helpers.
listThe scrolling list: the wrapping cursor, the window math, the row cursor, the separators, and the scrollbar.
rowsThe row grammar the settings pages and the drawers share (door, value, switch, action, info), the stat-ledger row, and the guarded action rows.
cardSelection, input, and drawing for the action rows of full-screen cards.
tilesThe rounded stat panes of the riding grid and the Fields editor, and the waypoint panel.
bandThe elevation band: the filled silhouette, the connected top stroke, and the peak label.
fmtOne formatter per printed quantity and output style.
marqueeThe one long name a frame scrolls, by whole characters.
pagerThe two-page auto-flip the detail compositions share.
sheetThe drawer sheets' shared motion and marks.
spinnerThe compass needle a working screen waits behind.

Long names end in two dots. One selected name per frame can scroll: a list row, detail title, or Peak View peak. Monospace fonts scroll by substring. The riding waypoint tile scrolls once per name change, then rests.

One palette, tuned to the 64-colour panel parchment wood ink amber warning route breadcrumb authored in RGB565 · resolved through the same color_fn as the map · every value asserted against its device-64 (RGB222) result MENU Routes Settings
All screen colors use the shared RGB565-to-RGB222 conversion.

Palette constants are written once and converted to the panel's 64 colors by the framebuffer, so a screen never picks a device color by hand.

Adding a screen

One module and one row declare a screen. The row alone does not put it on the glass:

StepWhere
The state type, handle, and draw.a new module under screen/
The module declaration, the variant, and its capabilities.screen/mod.rs: its mod line and the screens! table
Every string it prints, in four languages.the catalogs under i18n/
The way in: a menu, drawer or settings row, a companion card, or the module that owns the work.with that entry point, usually outside screen/
A seed, and the gestures that reach a page the first frame does not draw.harness/copy_fit.rs
A sweep frame, whose expect names the variant it must reach, and its digest row — one row per language for a langs frame.ui-frames.toml, then obc shot --accept records the digest in ui-snapshots.sha256

The acceptance test is the copy-fit gate over every reachable screen and language.

Source map

SubjectSource
Screen table, capabilities, contexts, and transitionsscreen/mod.rs
Gesture recognitioninput.rs, input_plane.rs
Repaint statedirty.rs, render_key.rs, ui_runtime.rs
Shared screen primitivesscreen/vocab/
Settings and translationssettings.rs, i18n/
Find a place and visitsfind_place.rs, visit.rs
POI and Up-ahead viewspoi_list.rs, whats_next.rs

See system architecture for the host loop and rendering pipeline for pixel generation.