Menu system for embedded C++. Zero overhead, composable, cross-platform (AVR, STM32, ESP32, native).
#include <oneMenu/oneMenu.h>
using namespace oneMenu;
InDef<LinuxKeyIn, PCKbd> in;
IOutDef<FullPrinter, ANSIFmt, DataParser<>, CtrlChars, Cursor<>, Gate, ANSIOut, ConsoleOut,
StaticPos<20,10>, StaticArea<30,8>> out;
bool quit(Sz) { running=false; return true; }
auto menu = menuDef<WrapNav>(
ItemDef<Text>{"My menu"},
staticBody(
ItemDef<Action<quit>, StaticText<&text::quit>>{},
)
);
INavDef<TreeNav, Root<menu>> nav;
void setup() {
out.lockMode(LockMode::None);
out.setColors(WHITE, BLACK);
out.clear();
nav.printTo(out);
}
bool run() {
nav.in(in);
if (nav.changed(out)) { nav.printTo(out); nav.sync(out); }
return running;
}Items are composed by listing components inside ItemDef<...> — those components wrap
each other; order matters, components higher in the list wrap those below. The item itself
is closed once assembled: a menu body holds a flat list of already-closed items (see
Body types), it doesn't fold/wrap them the way components inside a single
ItemDef<...> fold into each other.
| Component | Usage | Notes |
|---|---|---|
CText |
static constexpr CText name{"label"}; |
Compile-time string, zero RAM on AVR |
StaticText<&name> |
ItemDef<StaticText<&name>> |
Prints the CText constant |
Text |
ItemDef<Text>{"label"} |
Runtime const char*, set at construction |
static constexpr CText quit_text{"Exit!"};
using Quit = ItemDef<StaticText<&quit_text>>; // zero RAM label
ItemDef<Text>{"runtime label"} // set at runtime| Component | What it does |
|---|---|
AsLabel<...> |
Marks enclosed text as the item label (shown on the left) |
AsField<...> |
Marks enclosed data as the editable/selectable value |
AsUnit<...> |
Appends a unit string after the field value |
AsEditMode<> |
Shows an edit-mode indicator when the item is being edited |
ItemDef<AsLabel<StaticText<&text::power>>, // "Power"
AsField<Watch<Default<Int,55>>>, // 55
AsUnit<StaticText<&text::percent>>>{} // %| Component | What it does |
|---|---|
Watch<T> |
Wraps T, signals changed() when value changes (value-diff) |
Dirty<T> |
Wraps T, signals changed() after any set() (write-flag, not value-diff — matches AM4's prompt::dirty; used by compat/am4.h's FIELD() macro) |
Default<T, V> |
Wraps T, sets initial value to V |
Int |
Data<int> — integer storage |
Bool |
Data<bool> — bool storage |
EnDis<B> |
Enable/disable flag, initial state B (true=enabled) |
Watch<Default<Int, 55>> // int, starts at 55, change-notifies
Watch<EnDis<false>> // starts disabledId<N> // assigns compile-time integer id N to this item
Watch<EnDis<false>> // item starts disabledUse Id<N> to find and manipulate items at runtime:
auto& item = menu.find<SameAs<Id<N>>>(); // or: menu.find(byId<N>)
item.enable(false); // disable
item.enable(!item.enabled()); // toggleauto menu = menuDef<NavOptions...>(
title_item,
staticBody(item1, item2, ...)
);menuDef returns a value — the menu lives in the variable. Menus compose freely; pass one as a body item to create a submenu:
auto mainMenu = menuDef<WrapNav>(
ItemDef<Text>{"Main"},
staticBody(
menuDef<WrapNav>(
ItemDef<StaticText<&text::settings>>{},
staticBody(/* ... */)
),
Quit{}
)
);| Type | Characteristics |
|---|---|
StaticBody<I1,I2,...> / staticBody(i1,i2,...) |
Compile-time, heterogeneous items, zero overhead |
CArrayBody<T, arr, N> |
C array of same-type items, no virtual functions |
CPtrArrayBody<I, arr, N> |
Fixed-size array of I* pointers, virtual dispatch — heterogeneous and/or runtime-populated |
StdBody<Container> |
Any std::-container-shaped type (.size()/operator[]) — needs <vector>/heap, avoid on AVR |
// same-type C array — no virtual functions
CItem files[] = {"file1.txt", "file2.txt", "file3.txt"};
MenuDef<ItemDef<...>, CArrayBody<CItem, files, 3>>{};
// runtime-populated, no std::vector/heap-growth needed: a fixed-capacity
// IItem* array, left empty at declaration and assigned in setup() —
// CPtrArrayBody<IItem,arr,N>::depth() assumes 1 (leaf items only; see
// cArrayBody.h's own comment if a slot ever needs to open its own submenu)
IItem* items[3]{nullptr,nullptr,nullptr};
MenuDef<ItemDef<...>, CPtrArrayBody<IItem, items, 3>, Id<container_id>>{};
// then in setup:
items[0] = new IItemDef<Text>{"dynamic item"};StdBody<Container> still exists for a genuinely growable (not just fixed-capacity)
runtime list, but costs a real heap container — prefer CPtrArrayBody above whenever a
fixed upper bound on item count is acceptable (it almost always is: even AM4's own
"dynamic" menus are runtime-populated, not runtime-resized).
Fields are items that hold and display an editable value.
using Power = NumFieldDef<
Chain<Id<power_id>, AsLabel<StaticText<&text::power>>>, // label
NumField<
StaticNumRange<StaticRange<0,100,false>>, // range min,max,wrap
AsField<Watch<Default<Int,55>>> // data
>,
AsUnit<StaticText<&text::percent>> // unit
>;
Power{55} // construct with initial valueUp/Down keys change the value within range. false = clamp, true = wrap.
Inline text editing with cursor:
ItemDef<
AsLabel<Text>,
AsEditMode<>,
EditField,
ParentDraw,
AsField<TextField<15>> // max 15 chars
>{"Name"}Or with the alias:
using NameField = TextFieldDef<AsLabel<StaticText<&text::name>>, 15>;EditField — activates editing on Enter. ParentDraw — renders inline (no sub-level). TextField<N> — manages the character buffer and cursor.
Displays the current option inline. Each Enter advances to the next (wraps automatically).
using EdgeMode = ToggleFieldDef<
ItemDef<StaticText<&text::edge>, AsEditMode<>>,
StaticBody<
ItemDef<AsField<StaticText<&text::rise>>>,
ItemDef<AsField<StaticText<&text::fall>>>,
ItemDef<AsField<StaticText<&text::both>>>
>,
BodyAction<action::onChange>
>;
EdgeMode{}Opens the option list inline (no sub-level navigation). Selection shown on the item row.
using BrightnessSelect = SelectFieldDef<
ItemDef<AsLabel<StaticText<&text::brightness>>, AsEditMode<>>,
StaticBody<
ItemDef<AsField<StaticText<&text::s25>>, AsUnit<StaticText<&text::percent>>>,
ItemDef<AsField<StaticText<&text::s50>>, AsUnit<StaticText<&text::percent>>>,
ItemDef<AsField<StaticText<&text::s75>>, AsUnit<StaticText<&text::percent>>>
>,
WrapNav
>;Opens as a full submenu level. Chosen option shown on the item row after returning.
using ProfileSelect = ChooseFieldDef<
ItemDef<StaticText<&text::profile>, AsEditMode<>>,
StaticBody<
ItemDef<AsField<StaticText<&text::profile_a>>>,
ItemDef<AsField<StaticText<&text::profile_b>>>,
ItemDef<AsField<StaticText<&text::profile_c>>>
>,
WrapNav
>;Groups multiple fields into a single row (inline pad):
auto dateField(const char* lbl) {
return padDef(
ItemDef<AsLabel<Text>, AsEditMode<>>{lbl},
staticBody(
ItemDef<EditField, ParentDraw,
NumField<StaticNumRange<StaticRange<1900,2150,true>>,
AsField<Watch<Default<Int,2026>>>>>{2026},
ItemDef<StaticText<&text::dot>, EditField, ParentDraw, AsEditMode<>,
NumField<StaticNumRange<StaticRange<1,12,true>>, AsField<Watch<Int>>>>{1},
ItemDef<StaticText<&text::dot>, EditField, ParentDraw, AsEditMode<>,
NumField<StaticNumRange<StaticRange<1,31,true>>, AsField<Watch<Int>>>>{1}
)
);
}
// usage: dateField("date")Call visit(fn) on the field — the lambda receives the currently selected body ItemDef&. This is useful for reaching whatever real accessors that item exposes (enabled(), a Watch<>'s get(), etc.):
field.visit([](auto& item) { return item.enabled(); });Id<V> is a compile-time-only tag (used with find<> to locate an item — see
Runtime queries); it has no runtime accessor, so it can't be
used to recover which option was picked. For that, use the index BodyAction<fn>
already hands you and map it to your own enum:
enum Edge { RISING, FALLING, CHANGE };
bool onChange(Sz index) {
Edge e = static_cast<Edge>(index); // index matches body item order
return false;
}
using EdgeMode = ToggleFieldDef<
ItemDef<StaticText<&text::edge>, AsEditMode<>>,
StaticBody<
ItemDef<AsField<StaticText<&text::rise>>>, // index 0 → RISING
ItemDef<AsField<StaticText<&text::fall>>>, // index 1 → FALLING
ItemDef<AsField<StaticText<&text::both>>> // index 2 → CHANGE
>,
BodyAction<onChange>
>;Row/Rows arrange several independently-printable items (typically ItemDef<...>) on
one line or across a header/body/footer stack. Both are compile-time-static (partition
sizes are template params, not runtime state) — see Cross-platform setup
for the general zero-overhead philosophy this follows.
AlignLeft<II...> // = Align<AlignMethod::Left, II...>
AlignCenter<II...> // = Align<AlignMethod::Center, II...>
AlignRight<II...> // = Align<AlignMethod::Right, II...>Groups II... the same way Hidden<II...>/Decor<II...> do — used on its own (nothing
routing it through Row) it just prints its content plain, unaligned. Align carries no
rendering logic; the method only takes effect inside a Row.
using Test = ItemDef<Row<LeftItem,CenterItem,RightItem>>;Left/Center/Right must each be a standalone printable+constructible type (typically
an ItemDef<...>). Measures all three with LockMode::Measure dry runs before printing any
for real, so Center correctly lands in the gap between Left and Right — something a
plain Chain<Text,AlignCenter<Text>,AlignRight<Text>> can't do, since each Align only
ever sees out.free() at the moment it runs, never what a later sibling will still consume.
Falls back to plain sequential printing on non-cursor (streaming) devices.
using Screen = ItemDef<Rows<1, AlignMethod::Center, HeaderItem, BodyItem, FooterItem>>;
// sugars: RowsTop<bottomLines,...>, RowsCenter<...>, RowsBottom<...>Partitions the vertical space: Top prints, Bottom is pinned to the last bottomLines
rows (a footer), and Body is anchored within whatever's left via vMethod (Left=top,
Center, Right=bottom — reusing AlignMethod). Left/top needs no measurement; Center/
Right measure Body's own line count first via a LockMode::Measure dry run (the
vertical twin of Row's width measurement). bottomLines is a compile-time constant, not
runtime-adjustable — same static-partition rule as Row. Falls back to plain sequential
printing on non-cursor devices, where every item already spans the full row width anyway.
ItemDef<Liquid<10,3>, StaticText<&text::corner>>{} // compile-time position
ItemDef<LiquidPos, StaticText<&text::badge>> badge{};
badge.liquidPos({10,3}); // runtime-settable positionBoth save the cursor position, jump, print, then restore — so the next sequential item
isn't displaced. Meaningless on non-cursor (streaming) devices, so both fall back to a
plain in-sequence print there. Incompatible with ScrollPrinter/NoTitleScrollPrinter
(scroll measurement assumes items advance the cursor sequentially — enforced via
static_assert, not just documented); use FullPrinter/NoTitlePrinter instead.
Wraps an item so it claims all remaining vertical space after printing its own content
(pads down via Cursor::clearFree()/a native fillRect on GFX outputs). Combined with a
scroll-search printer, this turns a list into a single-item-per-page carousel — each item
gets the full display to itself as selection moves.
using VCenterDemo = ItemDef<FullScreen, CenterRow<AsField<StaticText<&text::alpha>>>>;
auto menu = menuDef<WrapNav>(
ItemDef<Text>{"FullScreen demo"},
staticBody(
ItemDef<FullScreen, CenterRow<AsField<StaticText<&text::alpha>>>>{},
ItemDef<FullScreen, CenterRow<AsField<StaticText<&text::beta>>>>{}
)
);Requires a printer built on aScrollBody (ScrollPrinter/NoTitleScrollPrinter/
SelectPrinter/NoTitleSelectPrinter — see Printers) — FullPrinter/
NoTitlePrinter would just pad trailing blank lines with no paging effect
(static_assert-enforced). For a strict "always show exactly the selected item, nothing
else" carousel, prefer SelectPrinter/NoTitleSelectPrinter over ScrollPrinter —
ScrollPrinter's general multi-item window search isn't built for a body where every
single item already consumes the whole page.
ANSIFmt/GfxFmt resolve colors (and GfxFmt also fonts) from a compile-time table that
cascades: set one value at the root and it applies everywhere, override only the branch
that needs to differ.
using MyPalette = Color<int>::Table<
/*Title*/ Color<int>::Colors<BLUE,WHITE>,
/*Default*/ Color<int>::Colors<WHITE,BLACK> // Default cascades to View, Nav, everything
>;
IOutDef<FullPrinter, ANSIFmt, DataParser<>, CtrlChars,
ColorTable<MyPalette>, // placed below ANSIFmt/GfxFmt
Cursor<>, Gate, ANSIOut, ConsoleOut,
StaticPos<0,0>, StaticArea<40,10>> out;Table shape (same for Color<Cor> and Font<Fnt>, just Colors<f,b> vs Value<v> leaves):
| Level | Meaning | Defaults from |
|---|---|---|
Table<Tit,Def,Vw,Nv> |
whole table | Def/Vw/Nv default to Tit |
Nav<En,Dis> |
enabled vs disabled | Dis defaults to En |
Enabled<It,Sel> |
item vs selected/focused | Sel defaults to It::Body |
Item<Bd,Fld,Ed> |
body/field/edit-mode role | Fld/Ed default to Bd |
Omit ColorTable<...>/FontTable<...> entirely to use the format's own built-in default
(ANSIFmt's reproduces the original hardcoded ANSI palette; GfxFmt's uses bool for
big/normal font selection, title big by default).
INavDef<TreeNav, Root<myMenu>> nav;Root<ref> binds the nav to an existing menu variable. StaticRoot<MenuType> owns the menu inside the nav instead.
| Component | What it does |
|---|---|
TreeNav |
Hierarchical navigator — tracks path, level, selection |
WrapNav |
Navigation wraps from last item to first and vice versa |
WrapNav is per-menu — add it to each menuDef<> where you want wrapping:
menuDef<WrapNav>(title, body) // wraps
menuDef<>(title, body) // clamps at endsToggleFieldDef always wraps (baked in).
nav.in(in); // process input
if (nav.changed(out)) {
nav.printTo(out); // render changed items
nav.sync(out); // mark as up-to-date
}A second nav component (add to NavDef<...>, not menuDef<...>) that adds async(path):
jump straight to an absolute index path ("/1/3/", 0-based, trailing /) from a cold
start, no prior nav state needed — built for HTTP, where each request arrives independent
of the last and the nav can't just remember "where it was".
NavDef<AsyncNav, TreeNav, Root<mainMenu>> webNav; // separate instance
NavDef<TreeNav, Root<mainMenu>> nav; // hardware nav, unaffected
webNav.async("/1/3/"); // reset to root, descend into submenu 1, select item 3
webNav.enter(); // then click — triggers the action or opens edit mode
if (webNav.async(path)) webNav.set(val); // or: navigate then set the now-focused field by stringGive the web nav its own NavDef<...> instance sharing the same Root<menu> as your
hardware nav — they read/write the same underlying OneData values (shared automatically,
since those live inside the menu struct) but keep independent cursor/level state, so a web
request never disturbs where the hardware nav currently is. See
Web output for the HTTP side of this.
out.lockMode(LockMode::None);
nav.printTo(out);Same shape as IOutDef/OutDef: a HAPI chain of sources, first with a pending command
wins. available()/cmd() are queried in list order.
InDef<LinuxKeyIn, PCKbd> in; // native/host testing: raw key codes + PC-keyboard mapping
nav.in(in); // feed into the navigator each loopA source's whole contract is two methods: bool available() (is there a pending event
right now) and CKE cmd() (return it — {Cmd::None} if there isn't one). That's it; no
tags, no base class to derive from beyond the plain struct/Part<O> shape every other
component here uses:
struct MyButtonIn {
template<typename I>
struct Part : I {
static bool available() { return digitalRead(PIN) == LOW; }
static CKE cmd() { return available() ? CKE{Cmd::Enter} : I::cmd(); }
};
};
InDef<MyButtonIn, PCKbd> in; // first source with a pending event winsparseKey(Key) (InAPI's third method, in.h) is only needed if your source reads raw
bytes that must be translated into a command (e.g. an ANSI escape sequence or a serial
byte stream) — most real sources (a button, an encoder, a pin read) never call it and can
leave the inherited no-op default alone.
nav.up()/down()/enter()/esc() are always callable directly, from anywhere — a
button ISR, a network command handler, a test — with no InDef/source involved at all.
This is the same mechanism the input chain itself calls internally, just invoked by hand:
void onNetworkCommand(const char* cmd) {
if (!strcmp(cmd,"up")) nav.up();
else if (!strcmp(cmd,"ok")) nav.enter();
}Reach for a real InDef source when you want the normal per-loop nav.poll()/nav.in()
cycle to pick up a device automatically; reach for direct calls when the trigger is already
arriving through its own path (an ISR, a callback, a test harness) and doesn't need to be
squeezed through the polling model at all — both are first-class, not one a workaround for
the other.
Bridge a real oneInput::InputDef<...> hardware chain (debounce/click/hold/encoder/
joystick filters — see OneInput's own README for the filter
stack itself) into CKE navigation commands:
| Component | Wraps | Emits |
|---|---|---|
BtnIn<HW, ClickCmd=Enter, HoldCmd=Esc> |
oneInput::BtnCapture (+Hold/Click/Debounce) |
ClickCmd on click, HoldCmd on hold |
EncIn<HW, Steps> |
oneInput::Encoder |
Up/Down per Steps detents |
JoyIn<HW> |
oneInput::Joystick (+ADC axes) |
Left/Right/Up/Down from deadzone-gated analog deltas |
using BtnHW = oneInput::InputDef<
oneInput::BtnCapture, oneInput::Hold<800>, oneInput::Click<300>, oneInput::Debounce<20>,
oneInput::avr::AvrBtnPin<1, chip::PortC, 2>
>;
using EncHW = oneInput::InputDef<oneInput::Encoder, oneInput::avr::AvrEncPins<1, chip::PortC, 0, 1>>;
ISR(PCINT1_vect) { BtnHW::dispatch(); EncHW::dispatch(); }
using Btn = oneMenu::BtnIn<BtnHW>;
using Enc = oneMenu::EncIn<EncHW, 4>;
InDef<Enc, Btn, PCKbd> in;The ISR (or a polled equivalent) drives the underlying HW::dispatch(); the *In<HW>
wrapper only translates already-captured events into CKE on each nav.in(in) poll.
Mirrors the output side's IOutDef/IOut/OutList split:
| Type | Shape |
|---|---|
IInDef<KK...> |
InDef<KK...> that also implements the virtual IIn interface |
InList<N> |
Runtime list of IIn* sources, polled in sequence, first with a pending event wins — a drop-in in object anywhere InDef<...> is used today |
InGroup<I, Ins...> |
Compile-time OR-combination of sources with no type erasure (unlike InList, not runtime-reconfigurable) |
IInDef<LinuxKeyIn> kbdSrc;
IInDef<oneMenu::BtnIn<BtnHW>> btnSrc;
InList<2> in(&kbdSrc, &btnSrc); // or default-construct + in.add(src) at runtime
nav.in(in);Use InList when the active device set isn't fixed at compile time (e.g. a hot-pluggable
secondary input); use plain InDef<...> (or InGroup for a type-erasure-free static OR)
otherwise — it costs nothing extra.
Neither auto-injects anything — both need the same explicit component list (printer,
format, parsers, Cursor<>/Gate/ColorTrack<> if the format needs them, device,
geometry). The only difference is IOutDef also implements the virtual IOut interface,
for code that needs to hold/pass an output by a common runtime-polymorphic type without
knowing its concrete component chain; OutDef has no virtual dispatch. Use OutDef unless
something specifically needs that runtime interface (e.g. a secondary output reached through
type-erased code).
IOutDef<FullPrinter, ANSIFmt, DataParser<>, CtrlChars, Cursor<>, Gate, ANSIOut, ConsoleOut,
StaticPos<20,10>, StaticArea<30,8>> out;
// secondary overlay, same shape, ColorTrack<> added for independent ANSI color state
OutDef<FullPrinter, ANSIFmt, DataParser<>, CtrlChars,
ColorTrack<int>, Cursor<>, Gate, ANSIOut, ConsoleOut,
StaticPos<24,12>, StaticArea<22,4>> promptOut;A device chain is a stack of components (OutDef<Printer, Fmt, ..., Device, Pos, Area>),
each layer adding one capability by wrapping the one below it. You only need to write the
bottom layer(s) that don't exist yet — printers/formats above are reused unchanged. Three
levels, each strictly additive over the last:
1. Plain text device — the minimum to print anything at all. Implement put(T) for
whatever value types you want to accept directly (char, const char*, int, ... —
anything without an overload just falls through to the next one via O::put(o)), plus
nl() and flush(). Wrap it in Raw::Part<...> and tag the type aRawDevice — this is
the same shape ConsoleOut/SerialOut (menu/IO/streamOut.h/menu/IO/arduino/serialOut.h)
already use:
struct MyRawOut : aRawDevice {
template<typename O>
struct RawPart : O {
using Base = O;
template<typename T> static void put(T o) { myDevice.write(o); O::put(o); }
static void nl() { myDevice.write('\n'); }
static void flush() { myDevice.flush(); }
};
template<typename O> using Part = typename Raw::template Part<RawPart<O>>;
};
// use: OutDef<FullPrinter, TextFmt, DataParser<>, CtrlChars, Cursor<>, Gate,
// MyRawOut, StaticPos<0,0>, StaticArea<40,4>> out;This alone is enough for TextFmt + any printer (FullPrinter/ScrollPrinter/...) — no
colors, no partial redraw beyond the basic changed() gate.
A device at this level gets setPos()/getPos() for free from OutAPI's own terminal
defaults — a silent no-op and a constant {0,0}, respectively — and that's genuinely safe
to leave alone, not just a convenient placeholder. Every real call site of getPos()/
setPos() anywhere in OneMenu's own printers/items (Row/Rows/Liquid/FullScreen in
item.h, the scroll printers in printers.h) is either wrapped in
if constexpr(hapi::query<IsCursor,typename Out::Types>) or belongs to a printer
component that itself static_assert(Requires<IsCursor,After>)s at composition time — a
chain without Cursor<> can't even be built with one of those printers, let alone reach
the call at runtime. So a pure-stream device's {0,0} is dead code from the framework's
own perspective; it only matters if your own code calls out.getPos() directly without
checking IsCursor first.
2. + cursor tracking — Cursor<CharW,LineH> (above) is required, not optional, the
moment you want more than the most trivial always-redraw-everything behavior:
ItemPrinter/ScrollBodyPrinter/SelectBodyPrinter all static_assert(Requires<IsCursor, ...>) on it. It's a ready-made component — you don't write it, just include it in the
chain, telling it your device's real character/line advance (CharW/LineH, or an Adv/
LnH function pointer for a variable-width/variable-height font). This is what gives you
getPos()/setPos()/clearToEOL()/free(), which the scroll/partial-redraw machinery
needs to reason about what's on screen.
3. + colors — add ColorTrack<Cor> (Cor = your color type, e.g. int for ANSI codes
or a small enum) above the raw device; it just remembers the last setColors(fg,bg) call
so resume() can restore it after an interruption — your raw device still needs its own
real setColors(Cor,Cor) implementation (ColorTrack forwards to Base::setColors, it
doesn't invent one). Pair with ANSIFmt (escape codes) or your own Fmt component driving
a Color<Cor>/ColorTable<...> cascading table (see
Cascading color/font tables).
4. + pixel/GFX drawing — tag your device aFillRect and implement fillRect(Pos,Area)
(and expose real pixel-based CharW/LineH/Adv to Cursor<>) for inverted-selection
rendering and clean-background-before-redraw (FullScreen, Rows's own clear step) —
see Pixel displays for a real device (OledOut) built this way,
and GfxFmt for the format pairing it expects. Nothing above this layer needs to know or
care that the device draws pixels instead of characters — same printer/format components
work unchanged, Cursor<> just gets pixel-scaled CharW/LineH instead of 1s.
None of this needs touching IOut, IItem, or INav — those are OneMenu's own internal
type-erasure boundaries (see the source comments in out.h/nav.h/item.h if you're
modifying OneMenu itself, not consuming it); adding a device is purely additive component
composition from the outside.
| Component | What it does |
|---|---|
FullPrinter |
Title + body + footer; body shows every item in order, no scrolling/windowing |
ScrollPrinter |
Title + body + footer; body scrolls (window search) when it exceeds the area |
NoTitlePrinter |
Body + footer only, no title row — default shape for small displays |
NoTitleScrollPrinter |
NoTitlePrinter + scrolling body |
SelectPrinter |
Title + body + footer; body always shows exactly the selected item, nothing else |
NoTitleSelectPrinter |
SelectPrinter without the title row |
"Full" vs "Scroll" is about whether the body windows/searches for a scroll position when it
overflows the area — not about redraw frequency. Every printer here still goes through the
normal per-item lockMode()/changed() gating (see Colors and lock mode),
so partial updates (only redrawing items that actually changed) work the same under
FullPrinter as under ScrollPrinter on any device that supports it.
All six are Chain<ViewPrinter, MenuPrinter<..., ItemsPrinter>> compositions over the same
building blocks (TitlePrinter, BodyPrinter/ScrollBodyPrinter/SelectBodyPrinter) —
pick by title/no-title × body-fit strategy rather than hand-assembling the chain yourself.
SelectPrinter/NoTitleSelectPrinter are the ones FullScreen
items are meant to run under.
| Component | What it does |
|---|---|
ANSIFmt |
ANSI colors, cursor highlight, edit-mode indicators |
TextFmt |
Plain text cursors and decorations, no escape codes |
GfxFmt<Radius,Spacing,BigTitle> |
Pixel-display format (inverted-video selection, optional big title font) — see Pixel displays |
BtFmt |
Values-only compact format for BT/BLE payloads — see BT/BLE output |
XmlFmt / JsonFmt |
Whole-tree dump (paths, nav state, labels/fields) for a remote-viewer UI — see Web output |
HtmlFmt |
Complete HTML page per render, selection via CSS class, nav links embedded — see Web output |
DataParser<> |
Converts data values to characters |
CtrlChars |
Translates control characters (newline, clear, etc.) |
TextWrap |
Long text continues on next line |
Clip |
Keeps content inside the defined area |
ANSIFmt → DataParser<> → CtrlChars → [ColorTrack/Cursor/Gate] → ANSIOut → ConsoleOut/SerialOut
ANSIOut injects ANSI escape codes into the stream. ConsoleOut writes to stdout.
StaticPos<col, row> // top-left corner in character units
StaticArea<width, height> // display area in charactersRelative positioning (use after defining a prior output):
StaticPos<decltype(out)::orgX()+4, decltype(out)::orgY()+2>
StaticArea<decltype(out)::width()-8, 4>out.lockMode(LockMode::None); // force full redraw next printTo
out.setColors(WHITE, BLACK); // foreground, background
out.clear(); // erase the output area
out.resume(); // re-anchor device to tracked position/colorsFormat for GFX-capable devices (anything exposing fillRect, e.g. SSD1306/PCD8544 via
OledOut). Works in device-native coordinates — pixels horizontally, pages vertically for
SSD1306. Selection is shown as inverted video (fillRect + XOR'd font bytes); the plain
text NavCursor indicator is fully suppressed since inversion already marks the row.
Colors/fonts still go through the same Color<Cor>/Font<Fnt> cascading-table mechanism
as ANSIFmt (Cor=bool = inverted?, Fnt=bool = big/normal font).
using MyOled = oneIO::display::I2cOledWire<Wire, 5, 4>;
OledDisplay<MyOled> display; // ready-made: FullPrinter+GfxFmt<>+OledOut
OledDisplay<MyOled, GfxFmt<2,0,true>> big; // Radius, Spacing, BigTitleOledDisplay<Oled, GfxFmtT=GfxFmt<>, Extra...> and Nokia5110Display<Lcd, GfxFmtT, Extra...>
are ready-made OutDefs deriving Cursor<> advances and the default area from the driver
(kWidth/kHeight/charWidth/lineSpacing); Extra... overrides position/area (earlier
entries in the HAPI chain win). For a hand-built chain (e.g. pairing GfxFmt with
SelectPrinter for a FullScreen carousel, which
neither ready-made alias offers), compose OledOut<Oled> directly — see
menu/IO/IOP/oledOut.h.
LcdDisplay<LCD> is the raw device adapter for HD44780-compatible character LCDs (same
slot as ConsoleOut/SerialOut — swap freely inside a hand-built OutDef). LCD must
expose print(char)/print(const char*)/setCursor(col,row)/clear() plus static
cols/rows (used for IsArea).
using MyLcd = oneIO::display::I2cLcd<Twi, 0x27, 20, 4>; // TwiMaster, Addr, Cols, Rows
LcdOut<MyLcd> lcdDisplay; // ready-made OutDef, TextFmt+NoTitleScrollPrinter
LcdOut<MyLcd, NoTitlePrinter> smallLcdDisplay; // override the printer for a tiny deviceLcdOut<Lcd, Printer=NoTitleScrollPrinter> is the ready-made OutDef (Printer+TextFmt+
DataParser<>+Cursor<1,1>+LcdDisplay<Lcd>) — cols/rows come from the LCD driver type
itself (e.g. I2cLcd<...,Cols,Rows> exposes them as Cols/Rows static members), not from
LcdOut's own template params.
Two independent paths for mirroring field values out over BLE GATT characteristics — same distinction as the coarse-vs-fine choice elsewhere (per-field vs per-tree):
Per-field — tag the data component with oneData::BTRec<W, Id> (same composition shape
as Watch/Default, wraps freely):
using Ch1 = NumFieldDef<
Chain<AsLabel<StaticText<&text::ch1>>>,
NumField<StaticNumRange<StaticRange<0,100,false>>,
AsField<BTRec<Watch<DataRef<¤tCh1>>, btIds::ch1_bt_id>>>,
AsUnit<StaticText<&text::percent>>
>;Every print()/printItem() calls out.btWrite<Id>(get()) — a no-op unless the Out
chain composes a matching oneOutput::BtOut<Ble, Id>, so an untagged/unwired field costs
nothing. Ble is duck-typed (char_write/char_read/char_written, see oneBus::BleAPI)
— ESP32 and nRF52 backends both verified on real hardware. Inbound (peer write → set())
isn't wired yet; read Ble::char_written(Id)/char_read(Id,...) directly in your loop.
Per-tree — pair BtFmt (strips paths/labels/nav-cursor chrome, emits only Field/Data
values comma-separated) with oneMenu::BtOut<Ble,Id,BufSz> (accumulates the render into a
buffer, flushes as one characteristic write). BtDisplay<Ble,Id,BufSz> is the ready-made
OutDef for this:
BtDisplay<Ble, my_menu_bt_id> btOut;
nav.printTo(btOut); // whole visible subtree -> one characteristic write on flush()Use per-field for individually-addressable values pushed on change; per-tree for a compact whole-menu snapshot record.
Unlike BtFmt, XmlFmt/JsonFmt dump the whole visible tree — paths, nav-cursor/
edit-mode state, labels and field values — sized for an HTTP client to render a full UI, not
a small GATT characteristic.
XmlFmt— element-per-item (<menu>/<title>/<body>/<item>/<lbl>/<fld>...), nav state as attributes (ncur="@",mode="edit"...), item body text auto-wrapped inCDATA. Each element carries apath="/1/3/"attribute so a client-side XSLT can translate/route without walking ancestors.JsonFmt— same information as one JSON object per item, nav state as properties.HtmlFmt— skips XML/XSLT entirely and streams a complete HTML page directly per render: selection shown via a CSS class (NavCursorsuppressed, the class handles highlight) and the up/down/enter/esc nav links embedded in the page itself. Use this instead ofXmlFmt+XSLT when you'd rather own the markup outright than transform on the client.
WebOut (Arduino/ESP32 only, #include <oneMenu/menu/IO/arduino/webOut.h>) streams
directly to a WebServer via sendContent() — no host-side buffer, each put() sends
immediately. WebDisplay is the ready-made OutDef pairing FullPrinter+XmlFmt+WebOut:
WebServer webServer(80);
WebDisplay webDisplay;
webServer.on("/", []() {
webServer.setContentLength(CONTENT_LENGTH_UNKNOWN);
webServer.send(200, "text/xml", "");
webServer.sendContent("<?xml-stylesheet type=\"text/xsl\" href=\"/menu.xsl\"?>\n");
webNav.printTo(webDisplay);
});
webServer.on("/menu.xsl", []() {
webServer.send(200, "text/xsl", WebOut::xsl()); // built-in stylesheet, or serve your own
});WebOut::xsl() returns a minimal built-in XSLT stylesheet (dark monospace theme, up/down/
enter/esc links) that renders the XmlFmt output as HTML in the browser and auto-refreshes;
docs/menu.xsl in this repo is the same stylesheet kept as a standalone, more readable file
for editing — serve your own by pointing /menu.xsl at a customized copy instead of
WebOut::xsl().
Combine with AsyncNav for a
stateless per-request nav that doesn't fight your hardware nav over the same menu.
Called when the item is activated (Enter on a non-edit item). Return true to keep the menu open, false to close this level.
bool myAction(Sz index) {
// index = position of this item in its body
return true;
}
ItemDef<Action<myAction>, StaticText<&text::label>>{}Called when a value is confirmed in a field body (Toggle/Select/Choose). index is the selected body position.
bool onChange(Sz index) { /* react to new selection */ return false; }
ToggleFieldDef<..., StaticBody<...>, BodyAction<onChange>>Renders enclosed content to a secondary output when this item is focused. Used for description panels or footers.
template<typename... OO>
using Desc = OnFocus<typename Put<OO...>::template ToOut<decltype(footer), footer, Clear::yes>>;
ItemDef<StaticText<&text::op1>, Desc<StaticText<&text::desc_op1>>>{}Member calls on the menu (not free functions). Search the menu's title, its own
chain, and every nested submenu's body, to any depth — so Id<N> works whether
it's attached to a plain body item or to a submenu itself.
auto& item = menu.find<SameAs<Id<N>>>(); // by id, explicit predicate type
auto& item = menu.find(byId<N>); // same thing, no `<>` needed
auto& item = menu.find<TagIs<MyTag>>(); // by tag (anything deriving from MyTag)A miss is a compile error, not a null/empty result — if Id<N> isn't anywhere
in the tree, find<> simply won't compile for that N.
auto& item = mainMenu.find<SameAs<Id<op3_id>>>();
item.enable(false);
item.enable(!item.enabled());nav.changed(out) // true if any visible item changed since last sync
item.changed() // true if this item's value changed
nav.sync(out) // mark all as synced
item.sync() // mark this item as syncedchar promptMsg[48]{};
using RunFn = bool(*)();
RunFn activeRun;
bool mainRun() { /* nav loop */ return running; }
bool promptRun() { /* promptNav loop */ return running; }
void showPrompt(const char* msg) {
strncpy(promptMsg, msg, sizeof(promptMsg)-1);
activeRun = promptRun;
promptOut.lockMode(LockMode::None);
promptOut.setColors(BLACK, WHITE);
promptOut.clear();
promptNav.printTo(promptOut);
}
bool action::ok(Sz) {
promptOut.clear();
activeRun = mainRun;
out.lockMode(LockMode::None);
nav.printTo(out);
return true;
}The prompt menu title is ItemDef<Text>{promptMsg} — Text stores a pointer, so promptMsg changes are picked up at render time.
Any number of modes (prompt, idle, sub-app) follow the same pattern.
static SysTick::Period<30000> idleTimer; // 30 seconds — file scope
bool mainRun() {
bool input = nav.in(in);
if (input) idleTimer.reset();
if (nav.changed(out)) { nav.printTo(out); nav.sync(out); }
if (idleTimer) { idleTimer.reset(); showIdle(); }
return running;
}
bool idleRun() {
if (nav.in(in)) { // any key wakes up
idleTimer.reset();
activeRun = mainRun;
out.lockMode(LockMode::None);
nav.printTo(out);
}
return running;
}idleTimer must be file-scope (not local to mainRun) so idleRun can reset it on wake.
enum ids { op3_id };
// in menu: ItemDef<Id<op3_id>, Watch<EnDis<false>>, ...>
auto& op3 = mainMenu.find<SameAs<Id<op3_id>>>();
op3.enable(!op3.enabled());
nav.printTo(out); // or let the next changed() catch itmenuDef<Opts...>(title, body) — factory function, returns a value, menu stored in a variable.
MenuDef<Title, Body, Opts...>{} — direct type alias, construct inline as a body item (no external variable needed).
// as a body item:
MenuDef<ItemDef<StaticText<&text::sub>>, StaticBody<...>, WrapNav>{}padDef(label_item, body) creates a single row displaying all body items inline. Each body item uses EditField + ParentDraw so it edits without opening a sub-level. Use StaticText separators between columns.
#ifdef __AVR__
using SysTick = chip::SysTick0<>;
IOP_TIMER0_ISR(Board)
// InDef: SerialIn / OutDef: SerialOut
#elif defined(__arm__)
using SysTick = chip::SysTick<>;
IOP_SYSTICK_ISR(Board)
#else
struct SysTick {
template<uint32_t Ms> using Period = hw::Period<Ms>;
};
// InDef: LinuxKeyIn / OutDef: ConsoleOut
#endifThe 30fps loop pattern:
bool run() {
static SysTick::Period<30> fps;
if (fps) { fps.reset(); activeRun(); }
if (!fps) hw::delay_ms(fps.when() - hw::millis());
return running;
}