Well, it
is pointers, but, if you'd just like an example of how that can be done -- I have a public example here,
https://github.com/T3sl4co1l/Reverb/blob/master/menu.c(and related files)
This implements a basic 4-button interface, up/down navigates a menu, right/left enters/exits a submenu or activates the item, etc.
The confusing things about constructing this are, I think:
1. How do you pack everything into (an) array(s)? Does it have to be fixed-length, and, how can I make the compiler satisfied that my types match?!
2. How to interleave functions and data? (Partly semantics, partly documentation/coding style.)
3. How to format the data so it can be navigated? What does it even mean to navigate a menu?
1. The above example shows a flat array; although, it doesn't have many (...any??) submenus, but, uh, I recall at least that I designed it to handle that just fine?
One catch is, it's entirely in RAM. I think some values are mutated at runtime so that's kind of justified, but those could be (should, even?) special-cased so that (almost) everything is pulled from Flash instead (which on the AVR, is a special memory space with specifier keywords and special access functions). As I had plenty of RAM and just wanted to throw together the project, I didn't explore this further. (On an ARM, say, or other platform with flat memory space including the ROM/Flash, this is a lot easier.)
Array lengths are irrelevant as type is discarded by the void pointer; lengths are not used, but a "sentinel" entry is used to define the bottom of the list. This is also used as a header, containing information on the active menu itself (e.g. functions to execute on entering, moving, exiting it). Defaults could also be placed here, like if you want all sub-menus of one group to return to the group root, rather than going back level by level. (Or going back to main menu, but that would be easier handled as a default, no need to write that pointer into every submenu.)
2. Interleaving is... an open question.
The semantics are: you
can't exactly write lambdas into C. The style is: even if you did [inline functions / lambdas], the state variables those functions operate on might not share locality with where they're defined (i.e., in the menu data structure). I guess if I were doing it in JS for example, I wouldn't mind putting most functions (the piddly menu housekeeping stuff) inline, and maybe keeping meatier calls in a separate location? It's going to be a little confusing at times, even in the best of cases; code graciously, and add comments to point to relevant code or objects. (Remember you're not so much writing for the compiler, as for the next person/people who will read this; and often that person is yourself, however many years later, having long forgotten how you put it all together!)
3. Navigation, I think there are two ways that usually come to mind.
Your, maybe, first thought / a more naive approach? might be:
Okay, I have program execution as state variable, and when I jump into a function, that function is the whole thing that's active at the moment. It draws its menu items, it accepts input, etc. etc.
Well, first problem with that is -- if you jump endlessly in circles, between functions whose whole purpose is their entire experience (I/O and functionality), you're never returning, and stack overflow quickly ensues.
You can use function return to implement menu return, which is nice if you're doing a strict hierarchical design. Beware, it could be that, some time you want to implement a sub-menu that links to an otherwise distant sub-menu -- you're approaching some aspect of the system from two different directions, and they share that one menu in common, so, it would make sense to be able to navigate to it from both ways. Which one do you return to, the previous one, the connected one (which?), or what? What if it links back, and now you have circles again? So you could end up with bridges across your tree structure, or whole loops, and now you have
graph problems, not just menu problems.
The most common case being a toolbar / heading / table of contents sort of thing.
It would be nice to avoid duplicating all of that function logic. If the function can be broken down to a skeleton of operations, maybe the same function can be used all the time, and instead of jumping between functions, you stay in the same one but its parameters vary.
But you might need to make those parameters into function calls, because you need real code statements to perform diverse actions with it.
So, the route I took, was to attach those function calls to each menu item that needs them, or defaults for otherwise passive items. Type checking is disabled by use of a void pointer, and when a menu option is executed, the compiler simply assumes that the location being jumped to, actually has the function prototype specified.
So the,
} else if (t == MENU_FUNC) {
if (menuMenu[menuIndex + menuPosY].i.ptr != NULL) {
((void(*)(void))menuMenu[menuIndex + menuPosY].i.ptr)();
}
}
the cryptic little pile of parenthesis is not a lisp, but how you typecast a pointer to a function in C, namely of type
void foo(void). The outer
()() is taking the value of the first parens (the function type) and executing it as a function (the second parens are the parameter list).
And, there are safer ways to use data types here; again, I used voids partly for expedience. I do recall trying once long ago, and I couldn't convince the compiler that the circle of references was actually reasonable and
yes it's okay trust me; I'd probably solve that problem nowadays, but probably still have to settle for type declarations (including fixed length , which makes things less readable and harder to maintain...
As for string methods or whatnot -- that's viable too, I don't like it from a one-off coding perspective but it's certainly doable with more work.
I haven't done that for a menu yet, but it can offer excellent compression, which becomes attractive if you're using a whole lot of them. I have, however, used similar methods for image compression; consider this:
https://github.com/T3sl4co1l/st7735_gfxmostly this part,
https://htmlpreview.github.io/?https://github.com/T3sl4co1l/st7735_gfx/blob/master/compr.htmlnot that it's very
functional, but as an example of a sequential byte code sort of approach. Putting functions into that, or let alone string offsets and other coding features, can quickly develop into a DSP (domain specific programming language) where you've not only not solved the underlying complexity but added a convoluted layer on top of it.
So, it must be used carefully. Obviously if we're just talking about bytecode for image compression, or a very basic menu system, that's one thing, but be very careful about feature creep, as this is exactly the kind of point where a codebase can be expanded just about indefinitely over time. (Not saying "don't". Just that... there are stories.
Always code judiciously.
)
Tim