From e46ce683a04a14ea0c85e15987c708857ab28d4d Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Fri, 6 Mar 2026 19:17:20 +0100 Subject: [PATCH 1/7] Fix Tokens values (cherry picked from commit a2aa7b18f8624a14a60be43d2f88c541dd0107fe) --- lib_nbgl/include/nbgl_use_case.h | 2 +- lib_nbgl/src/nbgl_use_case.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib_nbgl/include/nbgl_use_case.h b/lib_nbgl/include/nbgl_use_case.h index 85c724967..4148892d0 100644 --- a/lib_nbgl/include/nbgl_use_case.h +++ b/lib_nbgl/include/nbgl_use_case.h @@ -29,7 +29,7 @@ extern "C" { * @brief when using controls in page content (@ref nbgl_pageContent_t), this is the first token * value usable for these controls */ -#define FIRST_USER_TOKEN 20 +#define FIRST_USER_TOKEN 50 /** * @brief value of page parameter used with navigation callback when "skip" button is touched, to diff --git a/lib_nbgl/src/nbgl_use_case.c b/lib_nbgl/src/nbgl_use_case.c index 7c7bc715d..feb648c39 100644 --- a/lib_nbgl/src/nbgl_use_case.c +++ b/lib_nbgl/src/nbgl_use_case.c @@ -108,6 +108,7 @@ enum { FIRST_WARN_BAR_TOKEN, LAST_WARN_BAR_TOKEN = (FIRST_WARN_BAR_TOKEN + NB_WARNING_TYPES - 1), }; +CCASSERT(NBGL_TOKENS, LAST_WARN_BAR_TOKEN + 1 < FIRST_USER_TOKEN); typedef enum { REVIEW_NAV = 0, From 573ab58266a74f061727f70e9f0c3a5b58542278 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Fri, 6 Mar 2026 19:21:25 +0100 Subject: [PATCH 2/7] Fix keyboard mode setup for Nano (cherry picked from commit 13cec5fb4a384a5e6852c84128a85ac4dbf767b4) --- lib_nbgl/src/nbgl_layout_keyboard_nanos.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib_nbgl/src/nbgl_layout_keyboard_nanos.c b/lib_nbgl/src/nbgl_layout_keyboard_nanos.c index 9138f832b..64312cd1d 100644 --- a/lib_nbgl/src/nbgl_layout_keyboard_nanos.c +++ b/lib_nbgl/src/nbgl_layout_keyboard_nanos.c @@ -69,7 +69,7 @@ int nbgl_layoutAddKeyboard(nbgl_layout_t *layout, const nbgl_layoutKbd_t *kbdInf keyboard->mode = MODE_LOWER_LETTERS; } else { - keyboard->mode = MODE_NONE; + keyboard->mode = kbdInfo->mode; } keyboard->callback = PIC(kbdInfo->callback); keyboard->lettersOnly = kbdInfo->lettersOnly; From 062828b9a30b2a53a7b910ae7dce38c31f105492 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Fri, 6 Mar 2026 19:07:58 +0100 Subject: [PATCH 3/7] Enable keyboard layout for Nano (cherry picked from commit 7bcd19bf77cd7abe7e599f3d27fad68fcbf3b5ed) --- lib_nbgl/include/nbgl_layout.h | 108 +++++++++++++++++---------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/lib_nbgl/include/nbgl_layout.h b/lib_nbgl/include/nbgl_layout.h index 1c3b3b707..acdf164e5 100644 --- a/lib_nbgl/include/nbgl_layout.h +++ b/lib_nbgl/include/nbgl_layout.h @@ -134,10 +134,12 @@ extern "C" { #define NB_MAX_LINES 12 #else // HAVE_SE_TOUCH +// maximum suggestion buttons for keyboard +#define NB_MAX_SUGGESTION_BUTTONS 8 // 7 pixels on each side -#define AVAILABLE_WIDTH (SCREEN_WIDTH - 2 * 7) +#define AVAILABLE_WIDTH (SCREEN_WIDTH - 2 * 7) // maximum number of lines in screen -#define NB_MAX_LINES 4 +#define NB_MAX_LINES 4 #endif // HAVE_SE_TOUCH @@ -318,6 +320,57 @@ typedef struct { const nbgl_icon_details_t **rowIcons; ///< array of nbRows icon } nbgl_layoutLeftContent_t; +/** + * @brief The different types of keyboard contents + * + */ +typedef enum { + KEYBOARD_WITH_SUGGESTIONS, ///< text entry area + suggestion buttons + KEYBOARD_WITH_BUTTON, ///< text entry area + confirmation button + NB_KEYBOARD_CONTENT_TYPES +} nbgl_layoutKeyboardContentType_t; + +/** + * @brief This structure contains info to build suggestion buttons + */ +typedef struct { + const char **buttons; ///< array of 4 strings for buttons (last ones can be NULL) + int firstButtonToken; ///< first token used for buttons, provided in onActionCallback (the next + ///< 3 values will be used for other buttons) + uint8_t nbUsedButtons; ///< the number of actually used buttons +} nbgl_layoutSuggestionButtons_t; + +/** + * @brief This structure contains info to build a confirmation button + */ +typedef struct { + const char *text; ///< text of the button + int token; ///< token of the button + bool active; ///< if true, button is active, otherwise inactive (grayed-out) +} nbgl_layoutConfirmationButton_t; + +/** + * @brief This structure contains info to build a keyboard content (controls that are linked to + * keyboard) + */ +typedef struct { + nbgl_layoutKeyboardContentType_t type; ///< type of content + const char *title; ///< centered title explaining the screen + const char *text; ///< already entered text + bool numbered; ///< if set to true, the text is preceded on the left by 'number.' + uint8_t number; ///< if numbered is true, number used to build 'number.' text + bool grayedOut; ///< (unused, kept for compatibility) + int textToken; ///< the token that will be used as argument of the callback when "cross" + ///< button is touched (when text is not empty) + union { + nbgl_layoutSuggestionButtons_t + suggestionButtons; /// used if type is @ref KEYBOARD_WITH_SUGGESTIONS + nbgl_layoutConfirmationButton_t + confirmationButton; /// used if type is @ref KEYBOARD_WITH_BUTTON + }; + tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played +} nbgl_layoutKeyboardContent_t; + #ifdef HAVE_SE_TOUCH /** @@ -400,57 +453,6 @@ typedef struct { tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played } nbgl_layoutButton_t; -/** - * @brief The different types of keyboard contents - * - */ -typedef enum { - KEYBOARD_WITH_SUGGESTIONS, ///< text entry area + suggestion buttons - KEYBOARD_WITH_BUTTON, ///< text entry area + confirmation button - NB_KEYBOARD_CONTENT_TYPES -} nbgl_layoutKeyboardContentType_t; - -/** - * @brief This structure contains info to build suggestion buttons - */ -typedef struct { - const char **buttons; ///< array of 4 strings for buttons (last ones can be NULL) - int firstButtonToken; ///< first token used for buttons, provided in onActionCallback (the next - ///< 3 values will be used for other buttons) - uint8_t nbUsedButtons; ///< the number of actually used buttons -} nbgl_layoutSuggestionButtons_t; - -/** - * @brief This structure contains info to build a confirmation button - */ -typedef struct { - const char *text; ///< text of the button - int token; ///< token of the button - bool active; ///< if true, button is active, otherwise inactive (grayed-out) -} nbgl_layoutConfirmationButton_t; - -/** - * @brief This structure contains info to build a keyboard content (controls that are linked to - * keyboard) - */ -typedef struct { - nbgl_layoutKeyboardContentType_t type; ///< type of content - const char *title; ///< centered title explaining the screen - const char *text; ///< already entered text - bool numbered; ///< if set to true, the text is preceded on the left by 'number.' - uint8_t number; ///< if numbered is true, number used to build 'number.' text - bool grayedOut; ///< (unused, kept for compatibility) - int textToken; ///< the token that will be used as argument of the callback when "cross" - ///< button is touched (when text is not empty) - union { - nbgl_layoutSuggestionButtons_t - suggestionButtons; /// used if type is @ref KEYBOARD_WITH_SUGGESTIONS - nbgl_layoutConfirmationButton_t - confirmationButton; /// used if type is @ref KEYBOARD_WITH_BUTTON - }; - tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played -} nbgl_layoutKeyboardContent_t; - /** * @brief The different types of extended header * From f9eedf056dca081c2a2c0ddad4fbb42f4558937e Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Mon, 9 Mar 2026 09:58:19 +0100 Subject: [PATCH 4/7] Add Nano keyMask index (cherry picked from commit 938f96029ed263c5c8b0920d4d226f6423e57107) --- lib_nbgl/include/nbgl_obj.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib_nbgl/include/nbgl_obj.h b/lib_nbgl/include/nbgl_obj.h index e9c6bd322..8bee82b36 100644 --- a/lib_nbgl/include/nbgl_obj.h +++ b/lib_nbgl/include/nbgl_obj.h @@ -48,6 +48,10 @@ extern "C" { #define KEYBOARD_KEY_WIDTH 14 #define KEYBOARD_KEY_HEIGHT 14 #define KEYBOARD_WIDTH (5 * KEYBOARD_KEY_WIDTH) + +#define BACKSPACE_KEY_INDEX 26 +#define VALIDATE_INDEX 27 +#define SHIFT_KEY_INDEX 28 #endif // SCREEN_SIZE_WALLET #endif // NBGL_KEYBOARD From 2b2a2114f6bbb181378996ebe3109806c2092dc4 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 19 Feb 2026 16:33:48 +0100 Subject: [PATCH 5/7] Add nbgl_useCaseKeyboard (cherry picked from commit 80808b7ac00c3c1d183e55ad93057c7734149df4) --- lib_nbgl/include/nbgl_use_case.h | 49 +++++++ lib_nbgl/src/nbgl_use_case.c | 213 ++++++++++++++++++++++++++++--- 2 files changed, 245 insertions(+), 17 deletions(-) diff --git a/lib_nbgl/include/nbgl_use_case.h b/lib_nbgl/include/nbgl_use_case.h index 4148892d0..ff7fd90ce 100644 --- a/lib_nbgl/include/nbgl_use_case.h +++ b/lib_nbgl/include/nbgl_use_case.h @@ -162,6 +162,14 @@ typedef void (*nbgl_actionCallback_t)(uint8_t page); */ typedef void (*nbgl_pinValidCallback_t)(const uint8_t *pin, uint8_t pinLen); +/** + * @brief prototype of keyboard buttons callback function + * @param content content to fill + * @param mask of keys to activate/deactivate on keyboard + */ +typedef void (*nbgl_keyboardButtonsCallback_t)(nbgl_layoutKeyboardContent_t *content, + uint32_t *mask); + /** * @brief prototype of content navigation callback function * @param contentIndex content index (0->(nbContents-1)) that is needed by the lib @@ -546,6 +554,47 @@ void nbgl_useCaseKeypad(const char *title, nbgl_callback_t backCallback); #endif // NBGL_KEYPAD +#ifdef NBGL_KEYBOARD +typedef struct { + const char *buttonText; + nbgl_callback_t onButtonCallback; +} nbgl_kbdButtonParams_t; + +typedef struct { + const char **buttons; ///< array of 4 strings for buttons (last ones can be NULL) + int firstButtonToken; ///< first token used for buttons, provided in onButtonCallback + nbgl_layoutTouchCallback_t + onButtonCallback; ///< callback to call when one of the buttons is pressed + nbgl_keyboardButtonsCallback_t + updateButtonsCallback; ///< callback to call when a key is pressed to update suggestions +} nbgl_kbdSuggestParams_t; + +typedef struct { + nbgl_layoutKeyboardContentType_t type; ///< type of content + const char *title; ///< centered title explaining the screen + char *entryBuffer; ///< already entered text + uint8_t entryMaxLen; ///< maximum length of text that can be entered + bool numbered; ///< if set to true, the text is preceded on the left by 'number.' + uint8_t number; ///< if numbered is true, number used to build 'number.' text + bool lettersOnly; ///< if true, only display letter keys and Backspace + keyboardMode_t mode; ///< keyboard mode to start with +#ifdef HAVE_SE_TOUCH + keyboardCase_t casing; ///< keyboard casing mode (lower, upper once or upper locked) +#else // HAVE_SE_TOUCH + bool enableBackspace; ///< if true, Backspace key is enabled + bool enableValidate; ///< if true, Validate key is enabled + uint8_t selectedCharIndex; +#endif // HAVE_SE_TOUCH + union { + nbgl_kbdSuggestParams_t + suggestionParams; /// used if type is @ref KEYBOARD_WITH_SUGGESTIONS + nbgl_kbdButtonParams_t confirmationParams; /// used if type is @ref KEYBOARD_WITH_BUTTON + }; +} nbgl_keyboardParams_t; + +void nbgl_useCaseKeyboard(const nbgl_keyboardParams_t *params, nbgl_callback_t backCallback); +#endif // NBGL_KEYBOARD + #ifdef HAVE_SE_TOUCH // use case drawing DEPRECATED void nbgl_useCaseHome(const char *appName, diff --git a/lib_nbgl/src/nbgl_use_case.c b/lib_nbgl/src/nbgl_use_case.c index feb648c39..30c04c1fe 100644 --- a/lib_nbgl/src/nbgl_use_case.c +++ b/lib_nbgl/src/nbgl_use_case.c @@ -55,6 +55,7 @@ // macros to ease access to shared contexts #define sharedContext genericContext.sharedCtx #define keypadContext sharedContext.keypad +#define keyboardContext sharedContext.keyboard #define reviewWithWarnCtx sharedContext.reviewWithWarning #define choiceWithDetailsCtx sharedContext.choiceWithDetails @@ -105,6 +106,8 @@ enum { DISMISS_WARNING_TOKEN, DISMISS_DETAILS_TOKEN, PRELUDE_CHOICE_TOKEN, + KEYBOARD_BUTTON_TOKEN, + KEYBOARD_CROSS_TOKEN, FIRST_WARN_BAR_TOKEN, LAST_WARN_BAR_TOKEN = (FIRST_WARN_BAR_TOKEN + NB_WARNING_TYPES - 1), }; @@ -135,15 +138,28 @@ typedef struct AddressConfirmationContext_s { #ifdef NBGL_KEYPAD typedef struct KeypadContext_s { - uint8_t pinEntry[KEYPAD_MAX_DIGITS]; - uint8_t pinLen; - uint8_t pinMinDigits; - uint8_t pinMaxDigits; - nbgl_layout_t *layoutCtx; - bool hidden; + uint8_t pinEntry[KEYPAD_MAX_DIGITS]; + uint8_t pinLen; + uint8_t pinMinDigits; + uint8_t pinMaxDigits; + nbgl_layout_t *layoutCtx; + bool hidden; + nbgl_pinValidCallback_t onValidatePin; } KeypadContext_t; #endif +#ifdef NBGL_KEYBOARD +typedef struct KeyboardContext_s { + nbgl_layoutKeyboardContent_t keyboardContent; + char *entryBuffer; + uint8_t entryMaxLen; + int keyboardIndex; + keyboardCase_t casing; + nbgl_layout_t *layoutCtx; + nbgl_keyboardButtonsCallback_t getSuggestionButtons; +} KeyboardContext_t; +#endif + typedef struct ReviewWithWarningContext_s { bool isStreaming; nbgl_operationType_t operationType; @@ -179,6 +195,9 @@ typedef struct { union { #ifdef NBGL_KEYPAD KeypadContext_t keypad; +#endif +#ifdef NBGL_KEYBOARD + KeyboardContext_t keyboard; #endif ReviewWithWarningContext_t reviewWithWarning; ChoiceWithDetailsContext_t choiceWithDetails; @@ -267,9 +286,6 @@ static nbgl_layoutTouchCallback_t onControls; static nbgl_contentActionCallback_t onContentAction; static nbgl_choiceCallback_t onChoice; static nbgl_callback_t onModalConfirm; -#ifdef NBGL_KEYPAD -static nbgl_pinValidCallback_t onValidatePin; -#endif // contexts for background and modal pages static nbgl_page_t *pageContext; @@ -421,9 +437,6 @@ static void reset_callbacks_and_context(void) onControls = NULL; onContentAction = NULL; onChoice = NULL; -#ifdef NBGL_KEYPAD - onValidatePin = NULL; -#endif memset(&genericContext, 0, sizeof(genericContext)); } @@ -1905,7 +1918,7 @@ static void keypadCallback(char touchedKey) nbgl_refreshSpecialWithPostRefresh(BLACK_AND_WHITE_FAST_REFRESH, POST_REFRESH_FORCE_POWER_ON); - onValidatePin(keypadContext.pinEntry, keypadContext.pinLen); + keypadContext.onValidatePin(keypadContext.pinEntry, keypadContext.pinLen); break; default: @@ -1930,6 +1943,68 @@ static void keypadGeneric_cb(int token, uint8_t index) } #endif +#ifdef NBGL_KEYBOARD +// called when a key is touched on the keyboard +static void keyboardCallback(char touchedKey) +{ + uint32_t mask = 0; + size_t textLen = strlen(keyboardContext.entryBuffer); + if (touchedKey == BACKSPACE_KEY) { + if (textLen == 0) { + // No action + return; + } + keyboardContext.entryBuffer[--textLen] = '\0'; + } + else { + keyboardContext.entryBuffer[textLen] = touchedKey; + keyboardContext.entryBuffer[++textLen] = '\0'; + } + if (keyboardContext.keyboardContent.type == KEYBOARD_WITH_SUGGESTIONS) { + // if suggestions are displayed, we update them at each key press + keyboardContext.getSuggestionButtons(&keyboardContext.keyboardContent, &mask); + } + else if (textLen >= keyboardContext.entryMaxLen) { + // entry length can't be greater, so we mask every characters + mask = -1; + } + nbgl_layoutUpdateKeyboardContent(keyboardContext.layoutCtx, &keyboardContext.keyboardContent); + nbgl_layoutUpdateKeyboard(keyboardContext.layoutCtx, + keyboardContext.keyboardIndex, + mask, + false, + keyboardContext.casing); + nbgl_refreshSpecialWithPostRefresh(BLACK_AND_WHITE_REFRESH, POST_REFRESH_FORCE_POWER_ON); +} + +static void keyboardGeneric_cb(int token, uint8_t index) +{ + UNUSED(index); + switch (token) { + case BACK_TOKEN: + onQuit(); + break; + case KEYBOARD_CROSS_TOKEN: + // Simulate a word of a single letter 'a' and trigger the backspace action + keyboardContext.entryBuffer[0] = 'a'; + keyboardContext.entryBuffer[1] = '\0'; + keyboardCallback(BACKSPACE_KEY); + break; + case KEYBOARD_BUTTON_TOKEN: + if (keyboardContext.keyboardContent.type == KEYBOARD_WITH_BUTTON) { + onAction(); + } + break; + default: + if ((keyboardContext.keyboardContent.type == KEYBOARD_WITH_SUGGESTIONS) + && (token >= keyboardContext.keyboardContent.suggestionButtons.firstButtonToken)) { + onControls(token, index); + } + break; + } +} +#endif + /** * @brief computes the number of tag/values pairs displayable in a page, with the given list of * tag/value pairs @@ -4582,8 +4657,7 @@ void nbgl_useCaseKeypad(const char *title, nbgl_layoutHeader_t headerDesc = {.type = HEADER_BACK_AND_TEXT, .separationLine = true, .backAndText.token = BACK_TOKEN, - .backAndText.tuneId = TUNE_TAP_CASUAL, - .backAndText.text = NULL}; + .backAndText.tuneId = TUNE_TAP_CASUAL}; int status = -1; if ((minDigits > KEYPAD_MAX_DIGITS) || (maxDigits > KEYPAD_MAX_DIGITS)) { @@ -4615,13 +4689,12 @@ void nbgl_useCaseKeypad(const char *title, // add keypad content status = nbgl_layoutAddKeypadContent( keypadContext.layoutCtx, title, keypadContext.hidden, maxDigits, ""); - if (status < 0) { return; } // validation pin callback - onValidatePin = validatePinCallback; + keypadContext.onValidatePin = validatePinCallback; // pin code acceptable lengths keypadContext.pinMinDigits = minDigits; keypadContext.pinMaxDigits = maxDigits; @@ -4631,5 +4704,111 @@ void nbgl_useCaseKeypad(const char *title, } #endif // NBGL_KEYPAD +#ifdef NBGL_KEYBOARD +/** + * @brief Draws a standard keyboard modal page for text input. It contains: + * - a navigation bar at the top (with back button) + * - a centered title + * - an entry area for the text being entered + * - the keyboard at the bottom + * - either a confirmation button or suggestion buttons above the keyboard (depending on + * type) + * + * @note The keyboard supports different modes (lowercase, uppercase, digits, special characters) + * and can be configured to show only letters or all characters. + * + * @param params pointer to a structure containing: + * - type: KEYBOARD_WITH_BUTTON or KEYBOARD_WITH_SUGGESTIONS + * - title: string to display as page title + * - entryBuffer: buffer to store the entered text + * - entryMaxLen: maximum length of text that can be entered + * - mode: initial keyboard mode (MODE_LOWER, MODE_UPPER, MODE_DIGITS, MODE_SPECIAL) + * - lettersOnly: if true, only letter keys and Backspace are shown + * - numbered: (Touch only) if true, text is preceded by 'number.' + * - number: (Touch only) number used to build 'number.' prefix + * - casing: (Touch only) keyboard casing mode + * - confirmationParams: (if type is KEYBOARD_WITH_BUTTON) button configuration + * - suggestionParams: (if type is KEYBOARD_WITH_SUGGESTIONS) suggestions configuration + * @param backCallback callback called when the back button is pressed + */ +void nbgl_useCaseKeyboard(const nbgl_keyboardParams_t *params, nbgl_callback_t backCallback) +{ + // clang-format off + nbgl_layoutDescription_t layoutDescription = {.onActionCallback = keyboardGeneric_cb}; + nbgl_layoutHeader_t headerDesc = {.type = HEADER_BACK_AND_TEXT, + .backAndText.token = BACK_TOKEN, + .backAndText.tuneId = TUNE_TAP_CASUAL}; + nbgl_layoutKbd_t kbdInfo = {.callback = &keyboardCallback, + .lettersOnly = params->lettersOnly, + .mode = params->mode, + .casing = params->casing}; + int status = -1; + // clang-format on + + reset_callbacks_and_context(); + // reset the keyboard context + memset(&keyboardContext, 0, sizeof(KeyboardContext_t)); + + // memorize context + onQuit = backCallback; + + // get a layout + keyboardContext.layoutCtx = nbgl_layoutGet(&layoutDescription); + + // set back key in header + nbgl_layoutAddHeader(keyboardContext.layoutCtx, &headerDesc); + + // Add keyboard + status = nbgl_layoutAddKeyboard(keyboardContext.layoutCtx, &kbdInfo); + if (status < 0) { + return; + } + keyboardContext.keyboardIndex = status; + keyboardContext.casing = params->casing; + keyboardContext.entryBuffer = PIC(params->entryBuffer); + keyboardContext.entryMaxLen = params->entryMaxLen; + keyboardContext.entryBuffer[0] = '\0'; + // add keyboard content + keyboardContext.keyboardContent = (nbgl_layoutKeyboardContent_t){ + .type = params->type, + .title = PIC(params->title), + .numbered = params->numbered, + .number = params->number, + .text = keyboardContext.entryBuffer, + .textToken = KEYBOARD_CROSS_TOKEN, + .tuneId = TUNE_TAP_CASUAL, + }; + switch (params->type) { + case KEYBOARD_WITH_BUTTON: + onAction = PIC(params->confirmationParams.onButtonCallback); + keyboardContext.keyboardContent.confirmationButton = (nbgl_layoutConfirmationButton_t){ + .text = PIC(params->confirmationParams.buttonText), + .token = KEYBOARD_BUTTON_TOKEN, + .active = true, + }; + break; + case KEYBOARD_WITH_SUGGESTIONS: + onControls = PIC(params->suggestionParams.onButtonCallback); + keyboardContext.getSuggestionButtons + = PIC(params->suggestionParams.updateButtonsCallback); + keyboardContext.keyboardContent.suggestionButtons = (nbgl_layoutSuggestionButtons_t){ + .buttons = PIC(params->suggestionParams.buttons), + .firstButtonToken = params->suggestionParams.firstButtonToken, + }; + break; + default: + return; + } + status = nbgl_layoutAddKeyboardContent(keyboardContext.layoutCtx, + &keyboardContext.keyboardContent); + if (status < 0) { + return; + } + + nbgl_layoutDraw(keyboardContext.layoutCtx); + nbgl_refreshSpecialWithPostRefresh(FULL_COLOR_CLEAN_REFRESH, POST_REFRESH_FORCE_POWER_ON); +} +#endif // NBGL_KEYBOARD + #endif // HAVE_SE_TOUCH #endif // NBGL_USE_CASE From d006246b1311c40f38f47d10c97752ba01a937d4 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Wed, 4 Mar 2026 17:40:01 +0100 Subject: [PATCH 6/7] Add nbgl_useCaseKeyboard for Nano (cherry picked from commit 3d5d27565f651f514363458ac4302a21472cfbe4) --- lib_nbgl/include/nbgl_layout.h | 2 + lib_nbgl/include/nbgl_use_case.h | 86 +++++++----- lib_nbgl/src/nbgl_use_case_nanos.c | 212 +++++++++++++++++++++++++++++ 3 files changed, 263 insertions(+), 37 deletions(-) diff --git a/lib_nbgl/include/nbgl_layout.h b/lib_nbgl/include/nbgl_layout.h index acdf164e5..7b6bd8a6c 100644 --- a/lib_nbgl/include/nbgl_layout.h +++ b/lib_nbgl/include/nbgl_layout.h @@ -368,7 +368,9 @@ typedef struct { nbgl_layoutConfirmationButton_t confirmationButton; /// used if type is @ref KEYBOARD_WITH_BUTTON }; +#ifdef HAVE_SE_TOUCH tune_index_e tuneId; ///< if not @ref NBGL_NO_TUNE, a tune will be played +#endif } nbgl_layoutKeyboardContent_t; #ifdef HAVE_SE_TOUCH diff --git a/lib_nbgl/include/nbgl_use_case.h b/lib_nbgl/include/nbgl_use_case.h index ff7fd90ce..1f6c153ba 100644 --- a/lib_nbgl/include/nbgl_use_case.h +++ b/lib_nbgl/include/nbgl_use_case.h @@ -332,6 +332,55 @@ typedef struct { *prelude; ///< if not null, means that the review can start by a prelude } nbgl_warning_t; +#ifdef NBGL_KEYBOARD +/** + * @brief Structure containing configuration for keyboard with confirmation button + */ +typedef struct { +#ifdef HAVE_SE_TOUCH + const char *buttonText; ///< button title +#endif + nbgl_callback_t onButtonCallback; ///< callback to call when the button is pressed +} nbgl_kbdButtonParams_t; + +/** + * @brief Structure containing configuration for keyboard with suggestion buttons + */ +typedef struct { + const char **buttons; ///< array of strings for buttons (last ones can be NULL) + int firstButtonToken; ///< first token used for buttons, provided in onButtonCallback + nbgl_layoutTouchCallback_t + onButtonCallback; ///< callback to call when one of the buttons is pressed + nbgl_keyboardButtonsCallback_t + updateButtonsCallback; ///< callback to call when a key is pressed to update suggestions +} nbgl_kbdSuggestParams_t; + +/** + * @brief Structure containing all parameters for keyboard use case + * + * This structure is used to configure the keyboard modal page, including the title, + * input buffer, keyboard mode, and the type of content (with button or suggestions). + */ +typedef struct { + nbgl_layoutKeyboardContentType_t type; ///< type of content + const char *title; ///< centered title explaining the screen + char *entryBuffer; ///< already entered text + uint8_t entryMaxLen; ///< maximum length of text that can be entered + keyboardMode_t mode; ///< keyboard mode to start with + bool lettersOnly; ///< if true, only display letter keys and Backspace +#ifdef HAVE_SE_TOUCH + bool numbered; ///< if set to true, the text is preceded on the left by 'number.' + uint8_t number; ///< if numbered is true, number used to build 'number.' text + keyboardCase_t casing; ///< keyboard casing mode (lower, upper once or upper locked) +#endif + union { + nbgl_kbdSuggestParams_t + suggestionParams; /// used if type is @ref KEYBOARD_WITH_SUGGESTIONS + nbgl_kbdButtonParams_t confirmationParams; /// used if type is @ref KEYBOARD_WITH_BUTTON + }; +} nbgl_keyboardParams_t; +#endif // NBGL_KEYBOARD + /** * @brief The different types of operation to review * @@ -555,43 +604,6 @@ void nbgl_useCaseKeypad(const char *title, #endif // NBGL_KEYPAD #ifdef NBGL_KEYBOARD -typedef struct { - const char *buttonText; - nbgl_callback_t onButtonCallback; -} nbgl_kbdButtonParams_t; - -typedef struct { - const char **buttons; ///< array of 4 strings for buttons (last ones can be NULL) - int firstButtonToken; ///< first token used for buttons, provided in onButtonCallback - nbgl_layoutTouchCallback_t - onButtonCallback; ///< callback to call when one of the buttons is pressed - nbgl_keyboardButtonsCallback_t - updateButtonsCallback; ///< callback to call when a key is pressed to update suggestions -} nbgl_kbdSuggestParams_t; - -typedef struct { - nbgl_layoutKeyboardContentType_t type; ///< type of content - const char *title; ///< centered title explaining the screen - char *entryBuffer; ///< already entered text - uint8_t entryMaxLen; ///< maximum length of text that can be entered - bool numbered; ///< if set to true, the text is preceded on the left by 'number.' - uint8_t number; ///< if numbered is true, number used to build 'number.' text - bool lettersOnly; ///< if true, only display letter keys and Backspace - keyboardMode_t mode; ///< keyboard mode to start with -#ifdef HAVE_SE_TOUCH - keyboardCase_t casing; ///< keyboard casing mode (lower, upper once or upper locked) -#else // HAVE_SE_TOUCH - bool enableBackspace; ///< if true, Backspace key is enabled - bool enableValidate; ///< if true, Validate key is enabled - uint8_t selectedCharIndex; -#endif // HAVE_SE_TOUCH - union { - nbgl_kbdSuggestParams_t - suggestionParams; /// used if type is @ref KEYBOARD_WITH_SUGGESTIONS - nbgl_kbdButtonParams_t confirmationParams; /// used if type is @ref KEYBOARD_WITH_BUTTON - }; -} nbgl_keyboardParams_t; - void nbgl_useCaseKeyboard(const nbgl_keyboardParams_t *params, nbgl_callback_t backCallback); #endif // NBGL_KEYBOARD diff --git a/lib_nbgl/src/nbgl_use_case_nanos.c b/lib_nbgl/src/nbgl_use_case_nanos.c index 40c4e4499..40c5abbed 100644 --- a/lib_nbgl/src/nbgl_use_case_nanos.c +++ b/lib_nbgl/src/nbgl_use_case_nanos.c @@ -14,6 +14,7 @@ #include "nbgl_use_case.h" #include "glyphs.h" #include "os_pic.h" +#include "os_print.h" #include "os_helpers.h" #include "ux.h" @@ -117,6 +118,21 @@ typedef struct KeypadContext_s { } KeypadContext_t; #endif +#ifdef NBGL_KEYBOARD +typedef struct KeyboardContext_s { + nbgl_layoutKeyboardContent_t content; + char *entryBuffer; + uint16_t entryMaxLen; + nbgl_layout_t *layoutCtx; + uint8_t keyboardIndex; + uint8_t textIndex; + nbgl_callback_t actionCallback; + nbgl_callback_t backCallback; + nbgl_keyboardButtonsCallback_t getSuggestButtons; + nbgl_layoutTouchCallback_t onButtonCallback; +} KeyboardContext_t; +#endif + typedef enum { NONE_USE_CASE, SPINNER_USE_CASE, @@ -130,6 +146,7 @@ typedef enum { STATUS_USE_CASE, CONFIRM_USE_CASE, KEYPAD_USE_CASE, + KEYBOARD_USE_CASE, HOME_USE_CASE, INFO_USE_CASE, SETTINGS_USE_CASE, @@ -155,6 +172,9 @@ typedef struct UseCaseContext_s { ContentContext_t content; #ifdef NBGL_KEYPAD KeypadContext_t keypad; +#endif +#ifdef NBGL_KEYBOARD + KeyboardContext_t keyboard; #endif ActionContext_t action; }; @@ -1883,6 +1903,100 @@ static void keypadCallback(char touchedKey) } #endif // NBGL_KEYPAD +#ifdef NBGL_KEYBOARD +// Saved keyboard context for suggestion selection +// (needed because switching to CONTENT_USE_CASE overwrites the union context) +static struct { + const char **buttons; + int firstButtonToken; + uint8_t nbUsedButtons; + nbgl_layoutTouchCallback_t onButtonCallback; + nbgl_callback_t backCallback; +} savedKeyboardContext; + +// Navigation callback to fill the suggestion choices page +static bool suggestionNavCallback(uint8_t page, nbgl_pageContent_t *content) +{ + UNUSED(page); + content->type = CHOICES_LIST; + content->choicesList.names = savedKeyboardContext.buttons; + content->choicesList.token = savedKeyboardContext.firstButtonToken; + content->choicesList.initChoice = 0; + content->choicesList.nbChoices = savedKeyboardContext.nbUsedButtons; + return true; +} + +// Display suggestion selection page +static void displaySuggestionSelection(void) +{ + char title[20] = {0}; + // Save keyboard context before it gets overwritten + savedKeyboardContext.buttons = context.keyboard.content.suggestionButtons.buttons; + savedKeyboardContext.firstButtonToken + = context.keyboard.content.suggestionButtons.firstButtonToken; + savedKeyboardContext.nbUsedButtons = context.keyboard.content.suggestionButtons.nbUsedButtons; + savedKeyboardContext.onButtonCallback = context.keyboard.onButtonCallback; + savedKeyboardContext.backCallback = context.keyboard.backCallback; + + // Release the keyboard layout + nbgl_layoutRelease(context.keyboard.layoutCtx); + context.keyboard.layoutCtx = NULL; + + snprintf(title, sizeof(title), "Select word #%d", context.keyboard.content.number); + nbgl_useCaseNavigableContent(title, + 0, + 1, + savedKeyboardContext.backCallback, + suggestionNavCallback, + savedKeyboardContext.onButtonCallback); +} + +// called when a key is touched on the keyboard +static void keyboardCallback(char touchedKey) +{ + uint32_t mask = 0; + size_t textLen = strlen(context.keyboard.entryBuffer); + PRINTF("[keyboardCallback] touchedKey: '%c'\n", touchedKey); + if (touchedKey == BACKSPACE_KEY) { + if (textLen == 0) { + // Used to exit the keyboard when backspace is pressed on an empty entry + context.keyboard.backCallback(); + return; + } + context.keyboard.entryBuffer[--textLen] = '\0'; + } + else if (touchedKey == VALIDATE_KEY) { + context.keyboard.actionCallback(); + return; + } + else { + context.keyboard.entryBuffer[textLen] = touchedKey; + context.keyboard.entryBuffer[++textLen] = '\0'; + } + // Set the keyMask to disable some keys + if (context.keyboard.content.type == KEYBOARD_WITH_SUGGESTIONS) { + // if suggestions are displayed, we update them at each key press + context.keyboard.getSuggestButtons(&context.keyboard.content, &mask); + if ((context.keyboard.content.suggestionButtons.nbUsedButtons > 0) + && (context.keyboard.content.suggestionButtons.nbUsedButtons + < NB_MAX_SUGGESTION_BUTTONS)) { + // On Nano, when we have few suggestions, display a selection page + // instead of continuing with keyboard entry + displaySuggestionSelection(); + return; // Don't update keyboard, we're in suggestion mode + } + } + else if (textLen >= context.keyboard.entryMaxLen) { + // entry length can't be greater, so we mask every characters + mask = -1; + } + nbgl_layoutUpdateKeyboard(context.keyboard.layoutCtx, context.keyboard.keyboardIndex, mask); + nbgl_layoutUpdateEnteredText( + context.keyboard.layoutCtx, context.keyboard.textIndex, context.keyboard.entryBuffer); + nbgl_refresh(); +} +#endif + // this is the function called to start the actual review, from the initial warning pages static void launchReviewAfterWarning(void) { @@ -3056,5 +3170,103 @@ void nbgl_useCaseKeypad(const char *title, } #endif // NBGL_KEYPAD +#ifdef NBGL_KEYBOARD +/** + * @brief Draws a standard keyboard modal page for text input (Nano version). It contains: + * - navigation arrows (left/right) + * - a centered title at the top + * - an entry area for the text being entered + * - the keyboard navigable with buttons + * - either a confirmation button or suggestion buttons (depending on type) + * + * @note On Nano devices, the keyboard is navigated using physical buttons. + * The keyboard supports different modes (lowercase, uppercase, digits, special characters) + * and can be configured to show only letters or all characters. + * + * @param params pointer to a structure containing: + * - type: KEYBOARD_WITH_BUTTON or KEYBOARD_WITH_SUGGESTIONS + * - title: string to display as page title + * - entryBuffer: buffer to store the entered text + * - entryMaxLen: maximum length of text that can be entered + * - mode: initial keyboard mode (MODE_LOWER, MODE_UPPER, MODE_DIGITS, MODE_SPECIAL) + * - lettersOnly: if true, only letter keys and Backspace are shown + * - confirmationParams: (if type is KEYBOARD_WITH_BUTTON) button configuration + * - suggestionParams: (if type is KEYBOARD_WITH_SUGGESTIONS) suggestions configuration + * @param backCallback callback called when the back action is triggered + */ +void nbgl_useCaseKeyboard(const nbgl_keyboardParams_t *params, nbgl_callback_t backCallback) +{ + // clang-format off + nbgl_layoutDescription_t layoutDescription = {0}; + nbgl_layoutCenteredInfo_t centeredInfo = {.text1 = params->title, + .onTop = true}; + nbgl_layoutNavigation_t navInfo = {.direction = HORIZONTAL_NAV, + .indication = LEFT_ARROW | RIGHT_ARROW}; + nbgl_layoutKbd_t kbdInfo = {.callback = &keyboardCallback, + .mode = params->mode, + .lettersOnly = params->lettersOnly}; + int status = -1; + // clang-format on + + // reset the keypad context + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = KEYBOARD_USE_CASE; + context.currentPage = 0; + context.nbPages = 1; + context.keyboard.entryBuffer = PIC(params->entryBuffer); + context.keyboard.entryMaxLen = params->entryMaxLen; + context.keyboard.entryBuffer[0] = '\0'; + + context.keyboard.content.type = params->type; + + // memorize context + context.keyboard.backCallback = PIC(backCallback); + + switch (params->type) { + case KEYBOARD_WITH_BUTTON: + context.keyboard.actionCallback = PIC(params->confirmationParams.onButtonCallback); + break; + case KEYBOARD_WITH_SUGGESTIONS: + // No need for 'Validate' or 'Case switch' buttons + kbdInfo.keyMask = (1 << VALIDATE_INDEX) | (1 << SHIFT_KEY_INDEX); + context.keyboard.getSuggestButtons + = PIC(params->suggestionParams.updateButtonsCallback); + context.keyboard.onButtonCallback = PIC(params->suggestionParams.onButtonCallback); + context.keyboard.content.suggestionButtons = (nbgl_layoutSuggestionButtons_t){ + .buttons = PIC(params->suggestionParams.buttons), + .firstButtonToken = params->suggestionParams.firstButtonToken, + }; + break; + default: + return; + } + + // get a layout + context.keyboard.layoutCtx = nbgl_layoutGet(&layoutDescription); + + // add description + nbgl_layoutAddCenteredInfo(context.keyboard.layoutCtx, ¢eredInfo); + + // Add keyboard + status = nbgl_layoutAddKeyboard(context.keyboard.layoutCtx, &kbdInfo); + if (status < 0) { + return; + } + context.keyboard.keyboardIndex = status; + + // add empty entered text + status = nbgl_layoutAddEnteredText(context.keyboard.layoutCtx, "", true); + if (status < 0) { + return; + } + context.keyboard.textIndex = status; + + nbgl_layoutAddNavigation(context.keyboard.layoutCtx, &navInfo); + + nbgl_layoutDraw(context.keyboard.layoutCtx); + nbgl_refresh(); +} +#endif // NBGL_KEYBOARD + #endif // HAVE_SE_TOUCH #endif // NBGL_USE_CASE From 1721e2e5b764fe6f65113f05f4d81a3cc3e88f92 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Mon, 9 Mar 2026 10:46:27 +0100 Subject: [PATCH 7/7] Adapt doc (cherry picked from commit 449573ab7030a7362718ce5257519626f647145b) --- lib_nbgl/doc/nbgl_use_case.dox | 156 +++++++++++++++++++++++++++ lib_nbgl/doc/nbgl_use_case_nanos.dox | 144 +++++++++++++++++++++++++ 2 files changed, 300 insertions(+) diff --git a/lib_nbgl/doc/nbgl_use_case.dox b/lib_nbgl/doc/nbgl_use_case.dox index 3fb09b458..bec93894c 100644 --- a/lib_nbgl/doc/nbgl_use_case.dox +++ b/lib_nbgl/doc/nbgl_use_case.dox @@ -57,6 +57,8 @@ A few APIs are available to draw typical Use-Cases, such as: - @ref nbgl_useCaseAddressReview() to draw an address confirmation page, with a possibility to see it as QR Code and some extra tag/value pairs (see @subpage use_case_addr_review) - for keypad: - @ref nbgl_useCaseKeypad() to draw a default keypad implementation (see @subpage use_case_keypad) +- for keyboard: + - @ref nbgl_useCaseKeyboard() to draw a keyboard for text input (see @subpage use_case_keyboard) - for generic navigable content: - @ref nbgl_useCaseNavigableContent() to draw a level of generic content navigable pages @@ -729,6 +731,160 @@ void ui_menu_pinentry_display(unsigned int value) { } @endcode +@subsection use_case_keyboard Keyboard Use Case + +When text input is required (not just digits), a full keyboard can be displayed. +The keyboard supports different modes (lowercase, uppercase, digits, special characters) and can be configured +with either a confirmation button or dynamic suggestion buttons. + +As shown in the examples below, the keyboard page consists of: + +- a navigation bar at the top with a back button +- a title area +- an entry area displaying the text being entered +- the keyboard at the bottom (navigable with touch or buttons depending on device) +- either a confirmation button or suggestion buttons above the keyboard + +The @ref nbgl_useCaseKeyboard() function enables to create such page, with the following parameters: + +- a pointer to @ref nbgl_keyboardParams_t structure containing: + - \b type: either @ref KEYBOARD_WITH_BUTTON or @ref KEYBOARD_WITH_SUGGESTIONS + - \b title: the page title + - \b entryBuffer: buffer to store the entered text + - \b entryMaxLen: maximum length of text + - \b mode: initial keyboard mode (@ref MODE_LETTERS, @ref MODE_DIGITS, @ref MODE_SPECIAL) + - \b lettersOnly: if true, only letter keys and Backspace are shown + - \b numbered: (Touch only) if true, text is preceded by a number + - \b number: (Touch only) the number to display + - \b casing: (Touch only) keyboard casing mode + - \b confirmationParams or \b suggestionParams: depending on the type +- a callback for the back button + +@subsubsection use_case_keyboard_button Keyboard with Confirmation Button + +This variant is used when you simply need to validate the entered text with a button. + +Here is an example code to create a password nickname: + +@code +static char password_name[MAX_METANAME + 1] = {0}; + +static void create_password(void) { + // Process the entered password name + if (strlen(password_name) == 0) { + nbgl_useCaseStatus("The nickname\ncan't be empty", false, &display_menu); + } else { + // Create the password with the entered name + // ... + } +} + +static void back_to_menu(void) { + // Return to main menu +} + +void display_create_pwd(void) { + nbgl_kbdButtonParams_t confirmParams = { + .buttonText = "Create password", + .onButtonCallback = &create_password, + }; + + nbgl_keyboardParams_t keyboardParams = { + .type = KEYBOARD_WITH_BUTTON, + .title = "New password nickname", + .entryBuffer = password_name, + .entryMaxLen = sizeof(password_name), + .lettersOnly = false, + .mode = MODE_LETTERS, + .casing = LOWER_CASE, + .confirmationParams = confirmParams, + }; + password_name[0] = '\0'; + nbgl_useCaseKeyboard(&keyboardParams, &back_to_menu); +} +@endcode + +@subsubsection use_case_keyboard_suggestions Keyboard with Suggestions + +This variant is used when you want to provide dynamic suggestions as the user types. +This is particularly useful for word-by-word input, like entering a BIP39 recovery phrase. + +Here is an example code for entering BIP39 words: + +@code +#define MAX_WORD_LENGTH 8 +#define NB_MAX_SUGGESTION_BUTTONS 4 + +static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; +static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; +static const char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; + +enum { + FIRST_SUGGESTION_TOKEN = FIRST_USER_TOKEN + 10, +}; + +static void keyboard_dispatcher(const int token, uint8_t index) { + if (token >= FIRST_SUGGESTION_TOKEN) { + int buttonIndex = token - FIRST_SUGGESTION_TOKEN; + // Process the selected suggestion + add_word_to_phrase(buttonTexts[buttonIndex]); + // Continue to next word or finish + // ... + } +} + +static void update_buttons_callback(nbgl_layoutKeyboardContent_t *content, uint32_t *mask) { + size_t textLen = strlen(textToEnter); + content->number = get_current_word_number() + 1; + + if (textLen < 2) { + // No suggestions for less than 2 characters + content->suggestionButtons.nbUsedButtons = 0; + } else { + // Fill suggestions based on BIP39 word list + const size_t nbMatchingWords = bolos_ux_bip39_fill_with_candidates( + (unsigned char *)textToEnter, + textLen, + wordCandidates, + buttonTexts); + content->suggestionButtons.nbUsedButtons = nbMatchingWords; + } + if (textLen > 0) { + // Update keyboard mask to show only valid next characters + *mask = bolos_ux_bip39_get_keyboard_mask((unsigned char *)textToEnter, textLen); + } +} + +static void keyboard_back(void) { + // Handle back action +} + +void display_keyboard_page(void) { + nbgl_kbdSuggestParams_t suggestParams = { + .buttons = buttonTexts, + .firstButtonToken = FIRST_SUGGESTION_TOKEN, + .onButtonCallback = &keyboard_dispatcher, + .updateButtonsCallback = &update_buttons_callback, + }; + nbgl_keyboardParams_t keyboardParams = { + .type = KEYBOARD_WITH_SUGGESTIONS, + .title = "Enter word #1", + .entryBuffer = textToEnter, + .entryMaxLen = sizeof(textToEnter), + .numbered = true, + .number = 1, + .mode = MODE_LETTERS, + .casing = LOWER_CASE, + .lettersOnly = true, + .suggestionParams = suggestParams, + }; + textToEnter[0] = '\0'; + memset(buttonTexts, 0, sizeof(buttonTexts[0]) * NB_MAX_SUGGESTION_BUTTONS); + + nbgl_useCaseKeyboard(&keyboardParams, &keyboard_back); +} +@endcode + @section use_case_refresh_page Refreshing screen After having drawn graphic objects in **framebuffer**, all functions of this API automatically refresh the screen. diff --git a/lib_nbgl/doc/nbgl_use_case_nanos.dox b/lib_nbgl/doc/nbgl_use_case_nanos.dox index 9f4fa8ef8..3d078d5d7 100644 --- a/lib_nbgl/doc/nbgl_use_case_nanos.dox +++ b/lib_nbgl/doc/nbgl_use_case_nanos.dox @@ -56,6 +56,8 @@ A few APIs are available to draw typical Use-Cases, such as: - @ref nbgl_useCaseAddressReview() to draw an address confirmation page, with a possibility to see some extra tag/value pairs (see @subpage use_case_addr_review) - For keypad: - @ref nbgl_useCaseKeypad() to draw a default keypad implementation (see @subpage use_case_keypad) +- For keyboard: + - @ref nbgl_useCaseKeyboard() to draw a keyboard for text input (see @subpage use_case_keyboard) - For generic navigable content: - @ref nbgl_useCaseNavigableContent() to draw a level of generic content navigable pages @@ -627,6 +629,148 @@ void ui_menu_pinentry_display(unsigned int value) { } @endcode +@subsection use_case_keyboard Keyboard Use Case + +When text input is required (not just digits), a full keyboard can be displayed on Nano devices. +The keyboard is navigable using the physical buttons, and supports different modes (lowercase, uppercase, digits, special characters). + +The keyboard can be configured with either a confirmation button or dynamic suggestion buttons. + +The @ref nbgl_useCaseKeyboard() function enables to create such page, with the following parameters: + +- A pointer to @ref nbgl_keyboardParams_t structure containing: + - \b type: either @ref KEYBOARD_WITH_BUTTON or @ref KEYBOARD_WITH_SUGGESTIONS + - \b title: the page title + - \b entryBuffer: buffer to store the entered text + - \b entryMaxLen: maximum length of text + - \b mode: initial keyboard mode (@ref MODE_LETTERS, @ref MODE_DIGITS, @ref MODE_SPECIAL, @ref MODE_NONE) + - \b lettersOnly: if true, only letter keys and Backspace are shown + - \b confirmationParams or \b suggestionParams: depending on the type +- A callback for the back action + +@subsubsection use_case_keyboard_button_nano Keyboard with Confirmation Button (Nano) + +This variant is used when you simply need to validate the entered text with a button. + +Here is an example code to create a password nickname: + +@code +static char password_name[MAX_METANAME + 1] = {0}; + +static void create_password(void) { + // Process the entered password name + if (strlen(password_name) == 0) { + nbgl_useCaseStatus("The nickname\ncan't be empty", false, &display_menu); + } else { + // Create the password with the entered name + // ... + } +} + +static void back_to_menu(void) { + // Return to main menu +} + +void display_create_pwd(void) { + nbgl_kbdButtonParams_t confirmParams = { + .onButtonCallback = &create_password, + }; + + nbgl_keyboardParams_t keyboardParams = { + .type = KEYBOARD_WITH_BUTTON, + .title = "Create password", + .entryBuffer = password_name, + .entryMaxLen = sizeof(password_name), + .lettersOnly = false, + .mode = MODE_NONE, + .confirmationParams = confirmParams, + }; + password_name[0] = '\0'; + nbgl_useCaseKeyboard(&keyboardParams, &back_to_menu); +} +@endcode + +@subsubsection use_case_keyboard_suggestions_nano Keyboard with Suggestions (Nano) + +This variant is used when you want to provide dynamic suggestions as the user types. +This is particularly useful for word-by-word input, like entering a BIP39 recovery phrase. + +@note On Nano devices, suggestions are navigated using the same buttons as the keyboard. + +Here is an example code for entering BIP39 words: + +@code +#define MAX_WORD_LENGTH 8 +#define NB_MAX_SUGGESTION_BUTTONS 4 + +static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; +static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; +static const char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; + +enum { + FIRST_SUGGESTION_TOKEN = FIRST_USER_TOKEN + 10, +}; + +static void keyboard_dispatcher(const int token, uint8_t index) { + if (token >= FIRST_SUGGESTION_TOKEN) { + // On Nano, use both token and index to identify the suggestion + int buttonIndex = token - FIRST_SUGGESTION_TOKEN + index; + // Process the selected suggestion + add_word_to_phrase(buttonTexts[buttonIndex]); + // Continue to next word or finish + // ... + } +} + +static void update_buttons_callback(nbgl_layoutKeyboardContent_t *content, uint32_t *mask) { + size_t textLen = strlen(textToEnter); + content->number = get_current_word_number() + 1; + + if (textLen < 2) { + // No suggestions for less than 2 characters + content->suggestionButtons.nbUsedButtons = 0; + } else { + // Fill suggestions based on BIP39 word list + const size_t nbMatchingWords = bolos_ux_bip39_fill_with_candidates( + (unsigned char *)textToEnter, + textLen, + wordCandidates, + buttonTexts); + content->suggestionButtons.nbUsedButtons = nbMatchingWords; + } + if (textLen > 0) { + // Update keyboard mask to show only valid next characters + *mask = bolos_ux_bip39_get_keyboard_mask((unsigned char *)textToEnter, textLen); + } +} + +static void keyboard_back(void) { + // Handle back action (go to previous word or cancel) +} + +void display_keyboard_page(void) { + nbgl_kbdSuggestParams_t suggestParams = { + .buttons = buttonTexts, + .firstButtonToken = FIRST_SUGGESTION_TOKEN, + .onButtonCallback = &keyboard_dispatcher, + .updateButtonsCallback = &update_buttons_callback, + }; + nbgl_keyboardParams_t keyboardParams = { + .type = KEYBOARD_WITH_SUGGESTIONS, + .title = "Enter word #1", + .entryBuffer = textToEnter, + .entryMaxLen = sizeof(textToEnter), + .mode = MODE_LOWER_LETTERS, + .lettersOnly = true, + .suggestionParams = suggestParams, + }; + textToEnter[0] = '\0'; + memset(buttonTexts, 0, sizeof(buttonTexts[0]) * NB_MAX_SUGGESTION_BUTTONS); + + nbgl_useCaseKeyboard(&keyboardParams, &keyboard_back); +} +@endcode + @section use_case_refresh_page Refreshing screen After having drawn graphic objects in **framebuffer**, all functions of this API automatically refresh the screen.