I like to encode the menu structure in the data structure. So you might have:
typedef struct menu_s {
menu_func_rtn_t (*func)(int* this, int* params);
int* this;
menu_t prev;
menu_t next;
menu_t esc;
} menu_t;
(give or take syntax errors?)
The core loop being:
- Point to a given menu_t
- Call its function with its common data parameter this, and any other operands as needed (and implying how this might be syntactically simplified in C++!)
- The function performs menu-specific actions, when applicable
- The function returns a value determining the next action in the loop, e.g., an enum selecting the next menu_t
This echoes the above recommendation of an FSM; it's just encoded differently. Whereas you might implement one with a switch-case (the cases being effectively unnamed functions for each action), this has a core machine which operates on data; the data is written into a graph of objects.
There is a size-functionality tradeoff, and also a verbosity tradeoff (everything in the switch-case is just, well, in the switch-case, it's a huge pile; not that this can necessarily be organized any better, granted!). The switch-case (or even simpler, if-else) will win for short state machines; this format wins for generality, when you have a fair number of menus and a diversity of data and functionality on the items.
Of course if you need absolute minimum size, you can encode things even more tightly, say down to the level of pointers or offsets for target data and operations; or even writing a simple VM that runs bytecode, allowing extremely powerful (but probably also very cryptic) operations to be done in very little space; you'd want to write a code generator to facilitate such an extreme option).
I'm guessing you aren't pressed for RAM or ROM, so that kind of approach is already well out of scope; but if you get forced into such a corner, there are ways out.
The core loop might be at the level of a menu screen/page, where the function is executed as part of an init or data-fetch (say to put a value of interest into a string), or to change an option (in which case you might add a few fields to reference options where applicable), or on exit (to clean up?). Or multiple, however you like.
.this should contain some strings or graphics for each screen, or a list of items, or whatever. .next and .prev could be pages in a given menu, and .esc goes up one level. Sub-menus would have to be returned from .func, in which case it should have a different type, or a compound type (struct) .
On a limited system like a 4-digit display, each item might be a menu, and prev/next would go between items, since, well, items are screens...
The functions, you might craft a no-op or "dummy" function to handle the default case (e.g., navigation items). You might write functions specific to a given action, e.g.
"INCREMENT TEMPERATURE" -->
{
Temperature++;
updateTemperature();
return 0;
}
Or you might encode simple actions in this data, e.g.
{
(this.myPointer*)++;
this.update();
return 0;
}
You can slice it up however you like, and what's best will depend on what you need.
I don't know of any richly-featured systems offhand, but I doubt that there are any that are really general enough to fit anything anyone might want to do and are easy to build and are compact enough for embedded -- though again, if you're really not limited by memory, the overhead of that, plus e.g. STM32 HAL, plus an RTOS or something, might really just not matter. No idea!
Although there probably is a general and compact system (if not in existence, then possible to write), in C++, with copious use of templating; but setting them up may not be at all obvious, as a result (or maybe tool driven instead).
Tim