Skip to content

Modal Dialog Screens

The modal_dialog_screens module provides two full-screen modal dialog implementations for the Pibot CNC Pendant firmware UI: an input screen for interactive keyboard-based text and numeric entry, and a message box screen for informational, confirmation, and error dialogs. Both screens follow the same architectural patterns established by GenericScreen and UIManager, and are designed to coexist safely within LVGL’s single-threaded rendering loop on Core 1.

Both components are consumed across the entire application — from CNC-specific screens (jog, macros, settings) to system-level flows (Wi-Fi password, BT PIN, firmware update confirmations). They are never instantiated directly by application code; instead, dedicated convenience functions and a structured data object (InputScreenData / MessageBoxScreenData) serve as the public API.


modal_dialog_screens module

inputScreen namespace

(input_screen.h/cpp)

messageBoxScreen namespace

(message_box_screen.h/cpp)

GenericScreen

(generic_screen.h)

UIManager

(esp3d_ui.h)

VirtualButtonsComponent

(virtual_buttons_component.h)

customKeyboard

(custom_keyboard_component.h)

LVGL (lv_*)

Both namespaces (inputScreen, messageBoxScreen) build on top of GenericScreen, which provides the base LVGL screen object, the content container, and the VirtualButtonsComponent row along the bottom. The inputScreen additionally instantiates a customKeyboard component inside the container. UIManager is used for screen registration, unregistration, theme-aware styling, and orientation management.


creates

uses

creates

GenericScreen

+lv_obj_t* screen_

+lv_obj_t* container_

+VirtualButtonsComponent* virtual_buttons_

+prepareForDestruction()

+rotateContainer(angle)

+isValid() : bool

+getScreen() : lv_obj_t

+getContainer() : lv_obj_t

VirtualButtonsComponent

+update_button(idx, img, enabled)

+show_button(idx, show)

+simulate_click(idx)

+prepareForDestruction()

+isValid() : bool

KeyboardConfig

+lv_obj_t* parent

+lv_obj_t* textarea

+KeyboardType initial_type

+on_ok_cb_t on_ok

+on_cancel_cb_t on_cancel

+bool allow_leading_zeros

InputScreenData

+const char* title

+const char* default_value

+const char* unit

+KeyboardType keyboard_type

+float min_value

+float max_value

+uint8_t decimal_places

+uint8_t max_length

+bool allow_leading_zeros

+bool allow_empty

+ESP3DScreenType return_screen

+on_ok_callback()

+on_cancel_callback()

+prepare_current_screen_destruction()

MessageBoxScreenData

+MessageBoxType type

+const char* title

+const char* content

+ESP3DScreenType return_screen

+action_button_icon

+middle_button_icon

+on_ok_callback()

+on_yes_callback()

+on_no_callback()

+on_action_callback()

+on_middle_callback()

+on_screen_created_callback()

+prepare_current_screen_destruction()


inputScreen presents a full-screen input dialog with a title, a single-line textarea, and a virtual keyboard. It supports numeric, alphanumeric, and semicolon-separated list entry, with configurable range and decimal-place validation. It is the sole mechanism for collecting text or numeric values from the user across all screens.

KeyboardType enumDescriptionKey set
NumericStandard numeric input0–9, . (optional), - (optional), backspace
AlphanumericFull letter + symbol keyboardA–Z, 0–9, symbols, pages
NumericListFloatSemicolon-separated float list0–9, ., ;, backspace
NumericListIntSemicolon-separated integer list0–9, ;, backspace

For Numeric keyboards, the layout is further narrowed at runtime:

  • If decimal_places == 0 → decimal point key hidden → integer-only entry.
  • If min_value >= 0 → minus key hidden → positive-only entry.
  • If allow_leading_zeros == true → PIN mode; leading zeros are preserved and the LV_EVENT_VALUE_CHANGED normalisation handler is bypassed.
namespace inputScreen {
// Full configuration-driven entry point
void create(const InputScreenData& data);
// Convenience wrappers
void show_numeric_input(ESP3DScreenType return_to,
const char* title,
const char* default_value,
const char* unit,
void (*on_ok)(const char*, void*),
float min_value = 0.0f,
float max_value = FLT_MAX,
uint8_t decimal_places = 0,
void* user_data = nullptr,
void (*prepare_cleanup)(void) = nullptr);
void show_text_input(ESP3DScreenType return_to,
const char* title,
const char* default_value,
void (*on_ok)(const char*, void*),
uint8_t max_length = 32,
void* user_data = nullptr,
bool allow_empty = false,
void (*prepare_cleanup)(void) = nullptr);
void show_pin_input(ESP3DScreenType return_to,
const char* title,
const char* default_value,
void (*on_ok)(const char*, void*),
uint8_t pin_length = 4,
void* user_data = nullptr,
bool allow_empty = false,
void (*prepare_cleanup)(void) = nullptr);
} // namespace inputScreen

InputScreenData defaults — min_value = 0.0f, max_value = FLT_MAX, decimal_places = 0, max_length = 32, allow_leading_zeros = false, allow_empty = false, unit_sections = false.

For list keyboards (NumericListFloat, NumericListInt) a stored value can hold one list per unit, "mm;…;in;…" (jog_unit_lists.md, parsing in esp3d_unit_sections.h):

FieldRole
unit_sectionsEnables the mode; default_value may hold mm;…;in;…; unit is then ignored
unit_inchSection shown first (true = in)
unit_mm / unit_inUnit shown in the title while editing each section (default mm / in)
  • At creation parseUnitSections() splits the value; a legacy untagged list applies to both units and is edited in the section shown first.
  • The keyboard gets the mm/in toggle key (customKeyboard::enable_unit_toggle(), see ui_components.md). on_unit_toggle() keeps the edited text in its section, shows the other one and updates the title "<title> (<unit>)" (apply_title_text()).
  • On OK, finalize_unit_sections() validates each non-empty section as a numeric list, recomposes the list with composeUnitSections(), then checks emptiness and max_length; on_ok receives the full mm;…;in;… string. String operations are protected by try / catch.

Yes

valid

invalid

No

Yes

No

Yes

allow_empty=true

No

No

Yes

No

allow_empty=true

No

NumericListFloat

NumericListInt

Numeric

Alphanumeric

Yes

false

true

No

OK button released

unit_sections?

finalize_unit_sections

validate each section, recompose

Call on_ok callback

Error beep

no transition

Numeric type?

finalize_numeric_input

normalise edge cases

update_input_buffer from textarea

PIN mode?

allow_leading_zeros

length == 0?

length > max_length?

length == 0?

KeyboardType?

validate semicolon list

float values > 0

validate semicolon list

int values > 0

strtof parse

range check

decimal places check

length check only

custom_validator?

call custom_validator

writable copy

┌─────────────────────────────────┐
│ Title (+ unit) │ medium font, top-center
│ ┌───────────────────────────┐ │
│ │ Textarea (default_value) │ │ big font for numeric, medium for text/list
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ │ │
│ │ Custom Keyboard │ │ customKeyboard component
│ │ (type-dependent layout) │ │
│ │ │ │
│ └───────────────────────────┘ │
│ [OK] [ ] [Back] │ VirtualButtonsComponent
└─────────────────────────────────┘

messageBoxScreen presents a full-screen overlay with a typed title bar, a text content area, and up to three action buttons. Four dialog types are supported, each with distinct colour coding and button layout.

MessageBoxTypeTitle bar colourButton layoutTypical use
Informationaccent_active (blue)Centre: OKNotices, status messages
Confirmationaccent_select (darkened)Left: Yes, Right: NoDestructive action guard
Erroraccent_alert (red) + glow outlineCentre: OKFailure conditions
InformationWithActionaccent_active + glow outlineLeft: Action, Centre: Middle (opt), Right: OKTool change, file operations

When return_screen == ESP3DScreenType::none, buttons are suppressed entirely — this allows displaying a non-dismissible progress/status message box.

namespace messageBoxScreen {
// Full configuration-driven entry point
void create(const MessageBoxScreenData& data);
// Convenience wrappers
void show_information(ESP3DScreenType return_to, const char* title, const char* content,
void (*callback)(void*) = nullptr, void* user_data = nullptr,
void (*prepare_cleanup)(void) = nullptr);
void show_confirmation(ESP3DScreenType return_to, const char* title, const char* content,
void (*on_yes)(void*), void (*on_no)(void*) = nullptr,
void* user_data = nullptr,
void (*prepare_cleanup)(void) = nullptr);
void show_error(ESP3DScreenType return_to, const char* title, const char* content,
void (*callback)(void*) = nullptr, void* user_data = nullptr,
void (*prepare_cleanup)(void) = nullptr);
} // namespace messageBoxScreen
┌──────────────────────────────────┐
│ ╔═══════════ Title ════════════╗ │ accent-coloured title bar (type-dependent)
│ ╚═════════════════════════════╝ │
│ │
│ │
│ Content text │ wrapping label, centred, medium font
│ │
│ │
│ [Action] [Middle] [OK] │ VirtualButtonsComponent (layout varies by type)
└──────────────────────────────────┘

Both screens implement an identical timer-based lifecycle to ensure safe LVGL object management. LVGL’s single-threaded constraint means that no LVGL object may be destroyed from inside an event callback; all transitions must be deferred via timers.

Both inputScreen::create() and messageBoxScreen::create() call ui_manager.beginScreenTransition() (a14): the caller screen is replaced through deferred timers, and the global signal tells code outside the screen flow (the critical alert) to wait. The message box registers with registerScreen(message_box, screen, prepareForDestruction). The critical alert itself opens a message box on top of any screen, using the destructor the active screen registered (firmware_status.md).

Creation Timer Chain (with caller cleanup)

Section titled “Creation Timer Chain (with caller cleanup)”

When a modal dialog is opened from an existing screen that must destroy itself first, a two-stage timer chain runs:

LVGL timer queueinputScreen / messageBoxScreenCaller ScreenLVGL timer queueinputScreen / messageBoxScreenCaller Screenexits LVGL callback context firstcreate(data) with prepare_current_screen_destruction setlv_timer_create(caller_cleanup_timer_cb, 0ms)caller_cleanup_timer_cb firesprepare_current_screen_destruction()lv_timer_create(creation_timer_cb, DELAY_MS)creation_timer_cb firescreate_now() - build LVGL objects, register with UIManager

When no caller cleanup is needed, create_now() is called directly without any timer.

Shared strings (shared_title, shared_placeholder, shared_unit) are populated before the timers fire so that const char* pointers passed from stack-allocated caller contexts remain valid across the delay.

When the user presses OK, Yes, No, Action, or Cancel, the following sequence runs:

Next ScreenLVGL timer queueinputScreen / messageBoxScreenButton release callbackNext ScreenLVGL timer queueinputScreen / messageBoxScreenButton release callbackloads new screen, LVGL deletes old oneon_ok_release / on_yes_release / on_action_release / etc.call user callback (on_ok_callback, on_yes_callback…)cleanup_timer_cb (0ms) - guard: is_prepared_for_destruction_cleanup_timer_cb firesprepareForDestruction() - destroy keyboard, clear bufferslv_timer_create(transition_timer_cb, DELAY_MS)transition_timer_cb firescreateScreen(return_screen)LV_EVENT_DELETE → onScreenDestroyunregisterScreen, delete GenericScreen instance

Guard flags prevent double-execution: is_prepared_for_destruction_ blocks any second button press, and cleanup_executed_ prevents prepareForDestruction() from running twice via the ESP3D_PREPARE_DESTRUCTION_GUARD macro.

VariablePurpose
transition_timerDeferred screen switch timer
cleanup_timerTriggers prepareForDestruction before transition
caller_cleanup_timerDeferred caller screen destruction (0 ms)
creation_timerDeferred create_now() after caller cleanup
is_prepared_for_destruction_Guards against double cleanup / late button presses
cleanup_executed_Guards prepareForDestruction idempotency
next_screen_targetStores target screen type across timer delay
shared_title, shared_placeholder, shared_unitKeep string data alive across timer gaps

Return Screen

Modal Dialog Screen

Caller Screen (e.g. settings_list_screen)

Build InputScreenData /

MessageBoxScreenData

Set prepare_current_screen_destruction

create()

Store data in current_data

Copy strings to shared_*

Timer chain → create_now()

Build LVGL layout

Register with UIManager

User input / button press

Validate / invoke callback

Timer chain → createScreen(return_screen)

createScreen() called

LVGL screen loaded


The following rules are enforced throughout both screens, consistent with the project’s LVGL constraints:

RuleImplementation
Never destroy LVGL objects inside event callbacksAll destruction deferred via lv_timer_create
Change detection before updating UIis_prepared_for_destruction_ gate on all button handlers
No blocking in LVGL taskAll timers are 0 ms or ESP3D_TRANSITION_SCREEN_DELAY_MS — no blocking waits
No dynamic allocation in hot pathsInput buffer uses std::string; keyboard created once per screen
Guard std::string allocationsstd::bad_alloc is caught in validate_input() custom validator path
Shared string storageshared_title, shared_placeholder, shared_unit survive timer gaps without dangling pointers
lv_obj_is_valid() before useApplied to textarea, keyboard, container before every LVGL call

Both screens are invoked from a wide range of application screens. Refer to the following module documentation for usage context:

Key callers include:

ScreenUses inputScreenUses messageBoxScreen
settings_list_screenWi-Fi SSID/password, BT PIN, server address/port, jog feedrates, stepsReset confirmation, forget Wi-Fi/BT
macros_screenMacro description editConfirm run / clear-all / delete
jog_screenCustom step value edit—
status_screenFeed/spindle override values—
files_screen (all firmware variants)File renameConfirm delete, confirm job launch
scan_bt_screenBT PIN entry—
wifi_scan_screenWi-Fi password entry—
change_tool_screen (all firmware variants)Tool offset probe valuesIgnore / probe choice
DependencyRole
GenericScreen (screens_architecture.md)Base screen, container, VirtualButtonsComponent factory
UIManager (ui_style_guide.md)Style application, screen registration, orientation angle
customKeyboardKey input, OK/Cancel events, PIN mode
ESP3DScreenTypeEnum identifying registered screens for UIManager
esp3d_log / esp3d_log_eDiagnostic output (verbose / error level)
LVGL lv_timer_*Deferred execution for safe object lifecycle

inputScreen::show_numeric_input(
ESP3DScreenType::settings_list, // return screen
"Jog feedrate", // title
current_feedrate.c_str(), // default value
"mm/min", // unit
[](const char* value, void* ud) { // OK callback
saveFeedrate(value);
},
1.0f, // min_value
10000.0f, // max_value
0, // decimal_places → integer mode
nullptr, // user_data
prepareForDestruction // caller cleanup
);
inputScreen::show_numeric_input(
ESP3DScreenType::jog,
"Step size",
"0.1",
"mm",
onStepSaved,
0.001f, 100.0f,
3, // 3 decimal places
user_data,
prepareForDestruction
);
inputScreen::show_pin_input(
ESP3DScreenType::settings_list,
translationService.translate(ESP3DLabel::bt_pin),
current_pin.c_str(),
onPinSaved,
4, // pin_length
nullptr,
true, // allow_empty → clearing the PIN means no authentication
prepareForDestruction
);
messageBoxScreen::show_confirmation(
ESP3DScreenType::macros,
translationService.translate(ESP3DLabel::clear_all),
translationService.translate(ESP3DLabel::clear_all_confirm),
onConfirmClearAll, // on_yes
nullptr, // on_no (just returns)
nullptr,
prepareForDestruction
);
messageBoxScreen::show_error(
ESP3DScreenType::main,
"Connection failed",
error_message.c_str()
// no callback, no prepare_cleanup needed
);
MessageBoxScreenData data(
"Tool Change", // title
message.c_str(), // content
ESP3DScreenType::none, // return_screen=none → no auto-dismiss
onProbeButtonClick, // action button (left)
nullptr, // middle button (unused)
onIgnoreButtonClick, // OK button (right)
user_data,
prepareForDestruction,
&probe_b, // action button icon
nullptr, // middle button icon
onScreenCreatedCallback, // called after LVGL layout is built
user_data
);
messageBoxScreen::create(data);

  • screens_architecture.md — full UI screen hierarchy, GenericScreen, UIManager, VirtualButtonsComponent.
  • screen_transitions_flow.md — timer-based transition pattern (ESP3D_TRANSITION_START, ESP3D_CLEANUP_TIMER_BODY macros).
  • Input_system.md — hardware button / encoder / touch routing into virtual buttons.
  • ui_style_guide.md — ThemeStyles, applyLabelStyle, applyTextareaInputStyle, colour tokens.
  • theme_palette.md — accent_active, accent_alert, accent_select, accent_action token values used in the message box title bars.
  • features_ui.md — which SKUs expose which input/dialog flows.