ScrollMenu - Creating Scrollable Lists and Menus
Welcome to the UNA SDK tutorial series! The ScrollMenu app demonstrates fundamental concepts of menu navigation on the UNA Watch platform. This tutorial focuses on building an application with a scrollable menu using hardware buttons, providing a foundation for more complex user interfaces.
As in the earlier tutorials, the app comes with a TouchGFX GUI and an LVGL GUI over the same service. The menu is the first widget that differs substantially between the two: TouchGFX composes it from Designer containers and a ScrollWheelWithSelectionStyle, LVGL uses the SDK’s WheelMenu. See The Same Menu in Two Toolkits.
What You’ll Learn
How to implement GUI applications with TouchGFX or LVGL
Handling hardware button events for menu navigation
Understanding the UNA app framework for interactive applications
Getting Started
Prerequisites
Before building the ScrollMenu app, you need to set up the UNA SDK environment. Follow the toolchain setup for complete installation instructions, including:
UNA SDK cloned (
git clone https://github.com/UNAWatch/una-sdk.git)ST ARM GCC Toolchain (from STM32CubeIDE/CubeCLT, not system GCC)
CMake 3.21+ and make
Python 3 with pip packages installed
Minimum requirements for ScrollMenu:
UNA_SDKenvironment variable pointing to SDK rootARM GCC toolchain in PATH
CMake and build tools
For the TouchGFX GUI:
TouchGFX Designer installed (see toolchain setup)
For the LVGL GUI:
The LVGL submodule checked out once:
git submodule update --init ThirdParty/lvgl(from the SDK root)
Building and Running ScrollMenu
Verify your environment setup (see toolchain setup for details):
echo $UNA_SDK # Should point to SDK root. # Note for backward compatibility with linux path notation it uses '/' which arm-none-eabi-gcc # Should find ST toolchain which cmake # Should find CMake
Navigate to the ScrollMenu directory:
cd $UNA_SDK/Docs/Tutorials/ScrollMenu
Build the application with the GUI of your choice:
# TouchGFX GUI mkdir build && cd build cmake -G "Unix Makefiles" ../Software/Apps/ScrollMenu-CMake make # LVGL GUI (from the tutorial directory again) cd .. && mkdir build-lvgl && cd build-lvgl cmake -G "Unix Makefiles" ../Software/Apps/ScrollMenuLVGL-CMake make
The app will start and display an initial menu with three items. Use the hardware buttons to navigate the menu and perform actions, demonstrating menu navigation patterns. The two builds appear in the launcher as ScrollMenu and ScrollMenuLVGL.
Running on Simulator
TouchGFX (Windows only):
Open
ScrollMenu.touchgfxin TouchGFX Designer and click Generate Code (F4) (do this once).Navigate to
ScrollMenu\Software\Apps\TouchGFX-GUI\simulator\msvsOpen
Application.vcxprojin Visual StudioPress F5 to start debugging and run the simulator
LVGL (Windows and Linux): a CMake project in Software/Apps/LVGL-GUI/simulator, built the same way as HelloWorld’s (see that tutorial); the executable is ScrollMenuLVGLSimulator.
In either simulator, use keyboard keys to simulate hardware buttons:
1 = L1 (Previous menu item)
2 = L2 (Next menu item)
3 = R1 (Perform action on selected item)
4 = R2 (Exit)
The simulator will display the scrollable menu with Counter, Increase, Decrease items. For detailed simulator setup and button mapping, see Simulator.
ScrollMenu App Overview
Navigation Flow
Start with menu displaying Counter, Increase, Decrease items
Press top left button (L1) → Select previous menu item
Press bottom left button (L2) → Select next menu item
Press top right button (R1) → Perform action on selected item:
Counter: Reset counter to 0
Increase: Increment counter
Decrease: Decrement counter
Press bottom right button (R2) → Exit the app
Architecture Components
The Service Layer (Backend)
Manages the application lifecycle
Handles communication with the GUI layer
Minimal implementation since no sensors are used
One copy in
Software/Libs, linked by bothScrollMenu-CMakeandScrollMenuLVGL-CMake
The GUI Layer (Frontend)
Built with TouchGFX or LVGL
Handles button events and screen transitions
Updates the display based on user input
Manages screen state and visual elements
Button Event Handling
The app responds to hardware button presses through the handleKeyEvent method in MainView.cpp (TouchGFX) or onKey in MainScreen.cpp (LVGL). L1 and L2 buttons navigate the menu by selecting previous or next items. R1 button performs the action associated with the currently selected menu item, such as resetting, incrementing, or decrementing the counter. R2 exits the application.
Menu Item Management
The menu displays items with different appearances for selected and unselected states:
Selected items are highlighted in the center of the menu (typically with different styling)
Unselected items are shown in the background with standard appearance
When updating dynamic content (like the counter value), both selected and unselected versions of the item are updated to maintain consistency when scrolling
Screen Updates
TouchGFX: after performing actions that change the menu display (such as updating the counter value), the invalidate() method is called on the menu and its items to refresh the display, ensuring changes are visible to the user.
LVGL: the wheel’s refresh() re-renders its slots from the item table, and LVGL redraws what changed on the next frame.
The Same Menu in Two Toolkits
Piece |
TouchGFX ( |
LVGL ( |
|---|---|---|
The menu |
|
|
Item appearance |
|
one |
Item texts |
text keys |
plain |
Fonts |
typographies |
the same three faces, converted by |
Navigate |
|
|
Which item |
|
|
Change the counter’s text |
|
the item’s |
The lens behind the selection |
|
drawn by the widget ( |
The scroll indicator |
|
drawn by the widget’s |
The LVGL screen in full, less the boilerplate shared with the earlier tutorials:
using SDK::LVGL::WheelMenu;
using Style = WheelMenu::Item::Style;
mItems[COUNTER] = { Style::Simple, mCounterText }; // a buffer, so it can show the count
mItems[INCREASE] = { Style::Simple, "Increase" };
mItems[DECREASE] = { Style::Simple, "Decrease" };
const WheelMenu::Fonts fonts = { &poppins_semibold_30, &poppins_medium_18, &poppins_italic_18 };
mMenu = std::make_unique<WheelMenu>(mRoot, mItems, ITEM_COUNT, fonts);
// ... in onKey():
case Btn::L1: mMenu->prev(); break;
case Btn::L2: mMenu->next(); break;
case Btn::R1:
switch (mMenu->selected()) {
case COUNTER: mCounter = 0; break;
case INCREASE: mCounter++; break;
case DECREASE: mCounter--; break;
}
snprintf(mCounterText, sizeof(mCounterText), "Counter: %d", mCounter);
mMenu->refresh();
break;
WheelMenu keeps a pointer to the item table for as long as it lives, so the table is a member of the screen, not a local. It is the same widget the RunLVGL activity app builds its menus from; Item::Style also offers Tip (a hint line under the text), Toggle (an on/off switch) and Icon (a bitmap beside the text), and setSlideMidCallback() reports the moment the incoming item takes the centre.
Size on the watch
Build |
|
GUI code (text) |
GUI RAM (bss) |
|---|---|---|---|
ScrollMenu (TouchGFX) |
295 KB |
275 KB |
88 KB |
ScrollMenuLVGL |
188 KB |
179 KB |
139 KB |
ScrollMenu app creation process
The steps below create the TouchGFX GUI. For the LVGL GUI, copy HelloWorld’s LVGL-GUI and HelloWorldLVGL-CMake, rename them, change APP_NAME and APP_ID, add the three fonts to assets/assets.json and regenerate, and write MainScreen.cpp as shown above.
Copy HelloWorld tutorial
Change naming: Rename project directory, cmake directory and name of the project in CMakeLists.txt; Also change APP_ID to something else. Step 2 in Creating New Apps gives commmands for generating your own APP ID programatically from the name.
Commit initial changes: it’s a good practice to use version control system like git
*Add text keys using TouchGFX Designer:
In TouchGFX Designer, go to the Texts tab
Click + Add Text to create new text entries
Add three text entries with the following properties:
Text Id: “Counter”, Alignment: Center, Typography: Poppins_Medium_25, Translation (GB): “Counter”
Text Id: “Increase”, Alignment: Center, Typography: Poppins_Medium_25, Translation (GB): “Increase”
Text Id: “Decrease”, Alignment: Center, Typography: Poppins_Medium_25, Translation (GB): “Decrease”
These will generate the text keys T_COUNTER, T_INCREASE, T_DECREASE used in the code
*Edit TouchGFX:
Rename
*.touchgfxtoMY_APP.touchgfxRename
ScrollMenu.touchgfx:163"Name": "MY_APP"Open the main screen in the designer
From the widget palette, locate and drag a Menu widget onto the main screen canvas
Name the menu widget
menu1in the properties panelConfigure the menu properties:
Set the number of items to 3
Customize the appearance of selected and unselected items (fonts, colors, etc.)
Set initial text for each item using the text keys (T_COUNTER, T_INCREASE, T_DECREASE)
Position and size the menu widget appropriately on the screen (typically centered)
Click Generate code
Edit MainView.hpp:
Add member variables for state tracking:
class MainView : public MainViewBase { uint8_t lastKeyPressed = {'\0'}; int counter = 0; public: MainView(); virtual ~MainView() {} virtual void setupScreen(); virtual void tearDownScreen(); protected: virtual void handleKeyEvent(uint8_t key) override; };
Edit MainView.cpp:
In
setupScreen(), configure the menu items:void MainView::setupScreen() { MainViewBase::setupScreen(); menu1.setNumberOfItems(3); menu1.getNotSelectedItem(0)->config(T_COUNTER); menu1.getNotSelectedItem(1)->config(T_INCREASE); menu1.getNotSelectedItem(2)->config(T_DECREASE); menu1.getSelectedItem(0)->config(T_COUNTER); menu1.getSelectedItem(1)->config(T_INCREASE); menu1.getSelectedItem(2)->config(T_DECREASE); menu1.invalidate(); buttons.setL1(ButtonsSet::NONE); buttons.setL2(ButtonsSet::NONE); buttons.setR1(ButtonsSet::NONE); buttons.setR2(ButtonsSet::AMBER); }
Implement
handleKeyEventfor menu navigation and actions:void MainView::handleKeyEvent(uint8_t key) { if (key == Gui::Config::Button::L1) { menu1.selectPrev(); } if (key == Gui::Config::Button::L2) { menu1.selectNext(); } if (key == Gui::Config::Button::R1) { int selected = menu1.getSelectedItem(); auto* counter_nosel_item = menu1.getNotSelectedItem(0); auto* counter_sel_item = menu1.getSelectedItem(0); touchgfx::Unicode::UnicodeChar buffer[32]; switch (selected) { case 0: // Reset counter counter = 0; touchgfx::Unicode::snprintf(buffer, 32, "Counter: %d", counter); counter_nosel_item->config(buffer); counter_sel_item->config(buffer); break; case 1: counter++; touchgfx::Unicode::snprintf(buffer, 32, "Counter: %d", counter); counter_nosel_item->config(buffer); counter_sel_item->config(buffer); // Increment counter break; case 2: // Decrement counter counter--; touchgfx::Unicode::snprintf(buffer, 32, "Counter: %d", counter); counter_nosel_item->config(buffer); counter_sel_item->config(buffer); break; } menu1.invalidate(); counter_nosel_item->invalidate(); counter_sel_item->invalidate(); } if (key == Gui::Config::Button::R2) { presenter->exit(); } }
Compile code using SDK setup instructions.
Understanding Menu Navigation
The ScrollMenu app demonstrates how to handle hardware button events for menu navigation. Key concepts include:
Button Event Processing
Button presses are captured in the
handleKeyEvent(uint8_t key)method (TouchGFX) or theLV_EVENT_KEYhandler (LVGL)L1 and L2 buttons navigate the menu (select previous/next item)
R1 button performs the action associated with the selected menu item
R2 button exits the app
Menu State Management
The app maintains current menu selection state
Menu items display dynamic content (counter value)
TouchGFX needs
invalidate()calls to refresh the display after changes; the LVGL wheel is re-rendered withrefresh()
Common Patterns and Best Practices
Button Handling
Map button IDs to meaningful actions consistently
Use state variables to track multi-press sequences
In TouchGFX, always call
invalidate()after visual changes
UI Updates
Keep event handlers lightweight to maintain responsiveness
Use appropriate widgets for different content types
Consider user feedback for button presses (visual/audio)
Code Organization
Separate UI logic from business logic
Use clear naming for button actions and screen states
Document button mappings in comments
Performance Considerations
Minimize work in event handlers
Batch UI updates when possible
Avoid blocking operations that could delay response
Key Code Insights for New Developers
MainView.cpp / MainScreen.cpp Structure
handleKeyEvent()/onKey()is the central point for user inputMenu navigation uses
menu1.selectPrev()/menu1.selectNext()ormMenu->prev()/mMenu->next()Menu actions update the counter and refresh the display
Menu Configuration
TouchGFX: the menu is configured in
setupScreen()with item texts and count; items are configured withconfig()LVGL: the menu is built in the screen’s constructor from an item table and a
Fontsstruct
State Tracking
Use member variables like
counterto maintain app stateUpdate menu item text dynamically based on state changes
Next Steps
Run the ScrollMenu app - Build and test the menu navigation
Modify menu items - Experiment with different menu actions
Add new menu items - Extend the app with additional menu options
Explore the widgets - Add text, images, or other elements; in LVGL try the
TipandToggleitem stylesStudy advanced examples - Look at apps with more complex navigation
Troubleshooting
Build Issues
Ensure TouchGFX Designer is properly installed
Check that all project files are generated correctly
For the LVGL build, check that
ThirdParty/lvglis populatedVerify CMake configuration matches your environment
Runtime Issues
Confirm button mappings in the simulator match hardware
Check log output for event handling errors
Test on actual hardware for button responsiveness
Common Mistakes
Forgetting to call
invalidate()after UI changes (TouchGFX)Incorrect button ID constants
Not handling all button states appropriately
The ScrollMenu app provides a solid foundation for understanding menu navigation on the UNA platform. Mastering these patterns will enable you to create engaging, responsive applications.