Refactoring Menu Logic

Hey everyone, I recently made a menu system using a TFT display and a teensy with a Rotary Knob. My code currently works however it's very hardcoded. I can't seem to find a way to remove the switch statement in drawMenuItems. I feel like I should be able to run it without that and be able to put all my menu data in arrays but can't seem to figure out how. Any help is appreciated.

#include <SdFat.h>
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <SPI.h>
#include "EncoderTool.h"
#include <vector>
using namespace EncoderTool;

// Encoder encoder;
PolledEncoder encoder;

// TIMER MODULE
const char *chapterSelect[] = {"Chapter 1", "Chapter 2", "Chapter 3", "Chapter 4", "Chapter 5", "Chapter 6", "Chapter 7", "Chapter 8", "Chapter 9", "Chapter 10", "Chapter 11"};
const int chapterSelectLength = 11;
const char *chapter1[] = {"Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7", "Level 8"};
const int chapter1Length = 8;
const char *chapter2[] = {"Level 1", "Level 2", "Level 3", "Level 4"};
const int chapter2Length = 4;
const char *chapter3[] = {"Level 1", "Level 2", "Level 3", "Level 4", "Level 5"};
const int chapter3Length = 5;
const int menuWidth = 240;
const int menuHeight = 240;
const int menuTitlePadding = 6;
const int menuItemSize = 3;
const int menuTitleSize = 4;
const int menuItemPadding = 16;
const int maxMenuItems = 5;
int menuCurrentIndex = 0;
int minMenuItem = 0;
int menuCurrentIndexItems = 11;
int currentMenu = 0;
int chapterSelected = -1;
const char **menu = chapterSelect;

constexpr uint8_t A = 3, B = 4, Btn = 2; // pins

#define TFT_CS 10
#define TFT_RST 29 // Or set to -1 and connect to Arduino RESET pin
#define TFT_DC 30

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// END TIMER MODULE

void drawTitle(String text, uint16_t color)
{
  // Background
  tft.drawRect(0, 0, menuWidth, menuTitleSize * 8 + menuTitlePadding * 2, 0x530f);
  tft.fillRect(0, 0, menuWidth, menuTitleSize * 8 + menuTitlePadding * 2, 0x530f);

  int x = (240 - (text.length() * menuTitleSize * 6)) / 2; // Center Title
  tft.setCursor(x, menuTitlePadding);
  tft.setTextColor(color);
  tft.setTextWrap(false);
  tft.setTextSize(menuTitleSize);
  tft.print(text);
}

void drawMenuItems(int selected)
{
  int numberOfItems;
  switch (chapterSelected)
  {
  case -1:
    menu = chapterSelect;
    numberOfItems = chapterSelectLength;
    break;
  case 0:
    menu = chapter1;
    numberOfItems = chapter1Length;
    break;
  case 1:
    menu = chapter2;
    numberOfItems = chapter2Length;
    break;
  case 2:
    menu = chapter3;
    numberOfItems = chapter3Length;
    break;
  default:
    break;
  }
  // Title
  if (selected == 0)
  {
    drawTitle("Chapter", ST77XX_WHITE);
  }

  // If we hit the high end
  if (selected >= numberOfItems)
  {
    Serial.println(selected);
    encoder.setValue(numberOfItems - 1);
    menuCurrentIndex = numberOfItems - 1;
    Serial.println("NO MORE");
    return;
  }

  // If we hit the low end
  if (selected < 0)
  {
    encoder.setValue(0);
    menuCurrentIndex = 0;
    Serial.println("TOO LOW");
    return;
  }

  // If we are at the bottom but have more items
  if (selected > maxMenuItems)
  {
    Serial.println("MAX HIT");
    minMenuItem = selected - maxMenuItems;
    Serial.print("MinMenuItem: ");
    Serial.println(minMenuItem);
  }
  else
  {
    minMenuItem = 0;
  }
  int menuIndex = 0;
  for (uint8_t i = minMenuItem; i < numberOfItems; i++)
  {
    Serial.println(i);
    int yBg = ((menuTitleSize * 8) + (menuTitlePadding * 2) + ((menuItemSize * 8) * menuIndex)) + (menuItemPadding / 2 * (menuIndex + 1)) - menuItemPadding / 2;
    if (i == selected)
    {
      tft.drawRect(0, yBg, menuWidth, (menuItemSize * 8) + menuItemPadding - 4, ST77XX_BLUE);
      tft.fillRect(0, yBg, menuWidth, (menuItemSize * 8) + menuItemPadding - 4, ST77XX_BLUE);
    }
    else
    {
      tft.drawRect(0, yBg, menuWidth, (menuItemSize * 8) + menuItemPadding - 4, ST77XX_BLACK);
      tft.fillRect(0, yBg, menuWidth, (menuItemSize * 8) + menuItemPadding - 4, ST77XX_BLACK);
    }
    int y = ((menuTitleSize * 8) + (menuTitlePadding * 2) + ((menuItemSize * 8) * menuIndex)) + menuItemPadding / 2 * (menuIndex + 1);
    tft.setCursor(5, y);
    tft.setTextColor(ST77XX_WHITE);
    tft.setTextWrap(false);
    tft.setTextSize(menuItemSize);
    tft.print(menu[i]);

    menuIndex++;
  }
}

void updateMenu(int value, int delta)
{
  if (encoder.valueChanged()) // do we have a new value?
  {
    if (encoder.getValue() > menuCurrentIndex)
    {
      menuCurrentIndex++;
    }
    else
    {
      menuCurrentIndex--;
    }

    Serial.print("Encoder at before: ");
    Serial.println(encoder.getValue());

    drawMenuItems(menuCurrentIndex);

    Serial.print("Encoder at after: ");
    Serial.println(encoder.getValue());
  }
}

void menuSelect(int state)
{
  if (state == LOW)
  {
    currentMenu = 1;
    chapterSelected = menuCurrentIndex;

    tft.fillScreen(ST77XX_BLACK);
    drawMenuItems(0);
    Serial.println("SELECTED: ");
    Serial.print(menuCurrentIndex);
  }
  Serial.print("Current button state: ");
  Serial.println(state == LOW ? "pressed" : "released");
}

void setup(void)
{
  Serial.begin(9600);
  Serial.println("Initialized");

  // TIMER SETUP
  tft.init(menuWidth, menuHeight); // Init ST7789 240x240
  tft.fillScreen(ST77XX_BLACK);
  encoder.begin(3, 4, 2);
  encoder.attachCallback(updateMenu);
  encoder.attachButtonCallback(menuSelect);
  // END TIMER SETUP

  Serial.println("done");

  drawMenuItems(0);
}

void loop()
{
  encoder.tick();
}

do you mean this code ?

  switch (chapterSelected)
  {
  case -1:
    menu = chapterSelect;
    numberOfItems = chapterSelectLength;
    break;
  case 0:
    menu = chapter1;
    numberOfItems = chapter1Length;
    break;
  case 1:
    menu = chapter2;
    numberOfItems = chapter2Length;
    break;
  case 2:
    menu = chapter3;
    numberOfItems = chapter3Length;
    break;
  default:
    break;
  }

you would need to have menu and numberOfItems in an array of struct for example and you could use chapterSelected+1 as an index in this array (why would you start at -1 and not 0 ?)

const char *chapterSelect[] = {"Chapter 1", "Chapter 2", "Chapter 3", "Chapter 4", "Chapter 5", "Chapter 6", "Chapter 7", "Chapter 8", "Chapter 9", "Chapter 10", "Chapter 11"};
const char *chapter1[]      = {"Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7", "Level 8"};
const char *chapter2[]      = {"Level 1", "Level 2", "Level 3", "Level 4"};
const char *chapter3[]      = {"Level 1", "Level 2", "Level 3", "Level 4", "Level 5"};

struct t_menus {
  const char ** menu;
  int numberOfItems;
} myMenus[] = {
  {chapterSelect, sizeof chapterSelect / sizeof *chapterSelect},
  {chapter1, sizeof chapter1 / sizeof * chapter1},
  {chapter2, sizeof chapter2 / sizeof * chapter2},
  {chapter3, sizeof chapter3 / sizeof * chapter3},
}; 

consider this partial implementation


const byte PinButs [] = { A1, A2 };
const unsigned Nbuts = sizeof(PinButs);
byte butSt [Nbuts];

char s [80];

// ---------------------------------------------------------
void
func (
    int i )
{
    sprintf (s, "%s: %d", __func__, i);
    Serial.println (s);
}

void
func_1_1 (
    int i )
{
    sprintf (s, "%s: %d", __func__, i);
    Serial.println (s);
}

// ---------------------------------------------------------
struct MenuItem {
    const char *text;
    MenuItem   *subMenu;
    void      (*func)(int);
    int         arg;

};

// -------------------------------------
MenuItem menuChap1_1 [] = {
    { "Action1.1", NULL,        func_1_1 },
    { }
};

// ---------------------------
MenuItem menuChap1 [] = {
    { "Level_1.1", menuChap1_1, NULL },
    { "Level_1.2", NULL,        func,  12 },
    { }
};

// -------------------------------------
MenuItem menuChap2 [] = {
    { "Action2.1", NULL,        func_1_1, 21 },
    { "Action2.2", NULL,        func_1_1, 22 },
    { "Action2.3", NULL,        func_1_1, 23 },
    { }
};

// -------------------------------------
MenuItem menuMain [] = {
    { "Chap1", menuChap1, NULL },
    { "Chap2", menuChap2, NULL },
    { }
};

// ---------------------------------------------------------
#define MaxMenu   10
MenuItem *menu  [MaxMenu] = { menuMain };
int       lvl [MaxMenu];
int       menuIdx;
int       menuIdxLst;

// -----------------------------------------------------------------------------
void menuDisp (void)
{
    MenuItem *p = menu [menuIdx];
    Serial.println (p [lvl [menuIdx]].text);
}

// ---------------------------------------------------------
void menuTop (void)
{
    menuIdx       = 0;
    lvl [menuIdx] = 0;
}

// ---------------------------------------------------------
#define NO_BUT  -1
int
butGet ()
{
    for (unsigned n = 0; n < Nbuts; n++)  {
        byte but = digitalRead (PinButs [n]);

        if (butSt [n] != but)  {
            butSt [n] = but;
            delay (10);     // debounce
            if (LOW == but)
                return n;
        }
    }
    return NO_BUT;
}

// ---------------------------------------------------------
void loop()
{
    int but = butGet ();

    if (NO_BUT == but)
        return;

    MenuItem *p = menu [menuIdx];

    switch (but)  {
    case 0:             // up
        lvl [menuIdx]++;
#if 0
        Serial.print ("  case 0 - ");
        Serial.println ((menu [menuIdx])[lvl [menuIdx]].text);
#endif
        if ((NULL == p [lvl [menuIdx]].text))
            lvl [menuIdx] = 0;
        break;

    case 1:             // select
        if (p [lvl [menuIdx]].subMenu) {
            menu  [menuIdx+1] = p [lvl [menuIdx]].subMenu;
            lvl [menuIdx+1] = 0;
            menuIdx++;
        }
        else if (p [lvl [menuIdx]].func)  {
            p [lvl [menuIdx]].func (p [lvl [menuIdx]].arg);
            menuTop ();
        }
            
        break;
    }

#if 0
    sprintf (s, "%s: menuIdx %d, lvl %d", __func__, menuIdx, lvl [menuIdx]);
    Serial.println (s);
#endif

    menuDisp ();
}

// ---------------------------------------------------------
void setup(void)
{
    Serial.begin(9600);

    for (unsigned n = 0; n < Nbuts; n++)  {
        pinMode (PinButs [n], INPUT_PULLUP);
        butSt [n] = digitalRead (PinButs [n]);
    }

    menuDisp ();
}

This worked perfectly. Didn't know about structs. Thanks for the help!

You could take it a step further and use a node tree. This will allow you to have a default menu and easily add more later.

what's a node tree?

how does a binary tree help with a menu?

Node trees are not always binary.
Consider the following: (Apologies for the crude list, it’s not easy to do on a phone)

Edit: indenting kinda sucks

Cooking Ingredients (parent node)
->Fruit (L1 child node, L1 parent node)
• Oranges (L2 child)
• Apples (L2 child)
• Mangos (L2 child)
->Liquids (L1 child, L1 parent)
• Milk (L2 child)
->Juice (L2 child, L2 parent)
•Orange Juice (L3 child)
•Apple Juice (L3 child)
•Pineapple Juice (L3 child)
->Soda (L2 child, L2 parent)
•Cola (L3 child)
•Pepsi (L3 child)
•Sprite (L3 child)

so a variable tree. isn't that more or less what i did?

My mistake, yes you did a variable tree it’s just not in the way I’m used to seeing it laid out.

how would you lay is out?

Nested.

myMenu = [
MenuItem(title: “Menu 1”, children: [MenuItem(title: “Child 1”), MenuItem(title: “Child 2”), MenuItem(title: “Child 3)] ), MenuItem(title: “Menu 2”, children: [MenuItem(title: “Child 1”), MenuItem(title: “Child 2”), MenuItem(title: “Child 3)] ),MenuItem(title: “Menu3”, children: [MenuItem(title: “Child 1”), MenuItem(title: “Child 2”), MenuItem(title: “Child 3)] )
]

It would be easier to read if indentation worked correctly

There would also need to be something to walk through them and add the parent nodes.

I would also use classes instead of structs

Use code tags

please show the struct definition

don't understand this notation (certainly not C, what language)
what is myMenu = [
what is MenuItem(title:

It’s just sudo code.

The language I am used to is Swift (iOS), I haven’t used C/C++ in almost a decade. The syntax is different but the fundamentals are the same.

I just tried to make a test sketch and its very apparent I do not remember the c++ syntax anymore lol. Even my old libraries look foreign to me now.

I’m sure I can figure out the syntax again but I really don’t see a need to. I don’t plan on going back to any of my old projects or start any new ones.

sudo is short for substitute user do in Unix - usually used to pose as an admin when executing commands

You probably meant Pseudocode ?

It’s not so difficult to master both Swift and C++ :slight_smile:

yes