Skip to content

Creating Custom UI Components

Relevant source files

This guide covers the patterns and best practices for creating reusable UI components in the PiBot CNC Pendant firmware. Components are self-contained UI elements that can be integrated into multiple screens, providing consistent behavior and appearance across the application.

This document focuses on component architecture, lifecycle management, and integration patterns. For information about creating complete application screens, see 11.2 Adding New Screens For UI framework fundamentals, see 2.4 UI Layer and 7.1 UIManager


The firmware provides several patterns for creating UI components, depending on their complexity and whether they need encoder input handling. The PanelComponent is a specialized container for interactive items that supports both navigation and edit modes.

Base Classes

Reusable Components

Component Support

UIManager (esp3d_ui.h) ui_manager singleton

ComponentType enum virtual_buttons circular_menu panel firmware_status

VirtualButtonsComponent (virtual_buttons_component.h) 3-button interface

CircularMenuComponent (circular_menu_component.h) Radial navigation menu

PanelComponent (panel_component.h) Item list with edit modes

FirmwareStatusComponent (firmware_status.h) Status indicator

RotaryBaseComponent (rotary_base_component.h) Abstract encoder handler

GenericScreen (generic_screen.h) Screen base class

Component Architecture Hierarchy

Sources:


Component TypeBase ClassPurposeRegistration Required
VirtualButtonsComponentNone3-button interface with hardware/virtual inputYes
CircularMenuComponentRotaryBaseComponentRadial navigation menu with encoderYes
PanelComponentRotaryBaseComponentGrid/List of items with focus and edit modesYes
FirmwareStatusComponentNoneStatus indicator with click handlerYes
GenericScreenNoneBase class providing standard container and button layoutYes

Sources:


All components follow a standardized two-phase destruction lifecycle to ensure safe cleanup in the face of asynchronous events and screen transitions.

Component handles events responds to encoder input updates display

Block new events Safety flag for callbacks Prevent race conditions

Component Lifecycle State Machine

Sources:

Each component maintains several flags to track its state:

FlagPurposeSet During
is_valid_Component is operationalConstructor (after setup) main/display/components/panel_component.cpp#58
is_prepared_for_destruction_Destruction initiated, block eventsprepareForDestruction()main/display/components/panel_component.cpp#67
transition_in_progress_Static flag to prevent concurrent screen swapsGenericScreen Constructor main/display/screens/generic_screen.cpp#60

Sources:


PanelComponent is a powerful reusable component used in screens that require item selection and value adjustment. It manages a collection of PanelItem objects, each of which can be focused or edited using the rotary encoder.

The PanelComponent uses a panel_callback_t to notify the parent screen of state changes, such as focus gains or value updates.

// panel_component.h
typedef void (*panel_callback_t)(int32_t item_id,
PanelCallbackType type,
const std::string &value,
int32_t direction);

Sources:

Items are added to the panel with a list of possible values. The component handles the cycling of these values and visual updates.

Sources:


Components that respond to rotary encoder input should inherit from RotaryBaseComponent.

RotaryBaseComponent Contract

Concrete Implementation

PanelComponent

processEncoderStep() override Handles Nav vs Edit logic

RotaryBaseComponent (rotary_base_component.h)

pure virtual: processEncoderStep()

Encoder Component Architecture

Sources:

The implementation must override processEncoderStep to define how clockwise/counter-clockwise rotations affect the component state. PanelComponent uses two modes: PANEL_NAVIGATION_MODE for switching items and PANEL_EDITING_MODE for changing values main/display/components/panel_component.h#34-35

Sources:


GenericScreen provides a base for screens that require a standard 3-button layout at the bottom, managed by VirtualButtonsComponent. It also creates a standard container_ for the main UI content.

UIManagerVirtualButtonsComponentGenericScreenUIManagerVirtualButtonsComponentGenericScreen"new VirtualButtonsComponent(screen | configs)""registerComponent(host_screen | virtual_buttons | this)""return instance""registerComponent(host_screen | generic_screen | this)"

GenericScreen Component Integration

Sources:


Constructors should use std::nothrow for dynamic allocations to prevent uncaught exceptions from crashing the LVGL task.

When creating a new component that uses the encoder, clear the physical encoder buffer to prevent “phantom” pulses from previous screens.

Set appropriate debounce and accumulation limits for encoder handling to ensure the UI feels responsive yet stable.

Always register components with UIManager in the constructor and unregister in prepareForDestruction(). This allows the UI system to manage the component lifecycle correctly.

Sources: