diff --git a/lib_nbgl/include/nbgl_content.h b/lib_nbgl/include/nbgl_content.h index 5dcbb47d2..47ad63ca9 100644 --- a/lib_nbgl/include/nbgl_content.h +++ b/lib_nbgl/include/nbgl_content.h @@ -123,8 +123,10 @@ typedef struct { * */ typedef enum { - ENS_ALIAS = 0, ///< alias comes from ENS - ADDRESS_BOOK_ALIAS ///< alias comes from Address Book + NO_ALIAS_TYPE = 0, + ENS_ALIAS, ///< alias comes from ENS + ADDRESS_BOOK_ALIAS, ///< alias comes from Address Book + QR_CODE_ALIAS ///< alias is an address to be displayed as a QR Code } nbgl_contentValueAliasType_t; /** @@ -134,7 +136,9 @@ typedef enum { typedef struct { const char *fullValue; ///< full string of the value when used as an alias const char *explanation; ///< string displayed in gray, explaing where the alias comes from - ///< if NULL, a default explanation is provided, depending of the type + ///< only used if aliasType is @ref NO_ALIAS_TYPE + const char *title; ///< if not NULL and aliasType is @ref QR_CODE_ALIAS, is used as title of + ///< the QR Code nbgl_contentValueAliasType_t aliasType; ///< type of alias } nbgl_contentValueExt_t; @@ -264,7 +268,14 @@ typedef struct nbgl_pageSwitchesList_s { typedef struct { const char *const *infoTypes; ///< array of types of infos (in black/bold) const char *const *infoContents; ///< array of contents of infos (in black) - uint8_t nbInfos; ///< number of elements in infoTypes and infoContents array + const nbgl_contentValueExt_t + *infoExtensions; ///< if not NULL, gives additional info on type field + ///< any {0} element of this array is considered as invalid + uint8_t nbInfos; ///< number of elements in infoTypes and infoContents array + uint8_t token; ///< token to use with extensions, if withExtensions is true and infoExtensions + ///< is not NULL + bool withExtensions; /// if set to TRUE and if infoExtensions is not NULL, use this + /// infoExtensions field } nbgl_contentInfoList_t; /** diff --git a/lib_nbgl/include/nbgl_layout.h b/lib_nbgl/include/nbgl_layout.h index 0fd7dca6d..c414ed91a 100644 --- a/lib_nbgl/include/nbgl_layout.h +++ b/lib_nbgl/include/nbgl_layout.h @@ -593,6 +593,11 @@ int nbgl_layoutAddTopRightButton(nbgl_layout_t *layout, int nbgl_layoutAddTouchableBar(nbgl_layout_t *layout, const nbgl_layoutBar_t *barLayout); int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switchLayout); int nbgl_layoutAddText(nbgl_layout_t *layout, const char *text, const char *subText); +int nbgl_layoutAddTextWithAlias(nbgl_layout_t *layout, + const char *text, + const char *subText, + uint8_t token, + uint8_t index); int nbgl_layoutAddSubHeaderText(nbgl_layout_t *layout, const char *text); int nbgl_layoutAddRadioChoice(nbgl_layout_t *layout, const nbgl_layoutRadioChoice_t *choices); int nbgl_layoutAddQRCode(nbgl_layout_t *layout, const nbgl_layoutQRCode_t *info); diff --git a/lib_nbgl/src/nbgl_layout.c b/lib_nbgl/src/nbgl_layout.c index 179716f86..fdfd8914a 100644 --- a/lib_nbgl/src/nbgl_layout.c +++ b/lib_nbgl/src/nbgl_layout.c @@ -878,6 +878,109 @@ static nbgl_container_t *addContentCenter(nbgl_layoutInternal_t *layoutInt, return container; } +static int addText(nbgl_layout_t *layout, + const char *text, + const char *subText, + uint8_t token, + uint8_t index, + bool withAlias) +{ + nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *) layout; + nbgl_container_t *container; + nbgl_text_area_t *textArea; + nbgl_text_area_t *subTextArea; + uint16_t fullHeight = 0; + + if (layout == NULL) { + return -1; + } + container = (nbgl_container_t *) nbgl_objPoolGet(CONTAINER, layoutInt->layer); + + // get container children + container->children = nbgl_containerPoolGet(withAlias ? 3 : 2, layoutInt->layer); + container->obj.area.width = AVAILABLE_WIDTH; + + if (text != NULL) { + textArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); + + textArea->textColor = BLACK; + textArea->text = PIC(text); + textArea->textAlignment = MID_LEFT; + textArea->fontId = SMALL_BOLD_FONT; + textArea->style = NO_STYLE; + textArea->wrapping = true; + textArea->obj.alignment = NO_ALIGNMENT; + textArea->obj.alignmentMarginY = PRE_TEXT_MARGIN; + textArea->obj.area.width = container->obj.area.width; + if (withAlias == true) { + textArea->obj.area.width -= 12 + MINI_PUSH_ICON.width; + } + textArea->obj.area.height = nbgl_getTextHeightInWidth( + textArea->fontId, textArea->text, textArea->obj.area.width, textArea->wrapping); + fullHeight += textArea->obj.area.height + textArea->obj.alignmentMarginY; + container->children[container->nbChildren] = (nbgl_obj_t *) textArea; + container->nbChildren++; + if (withAlias == true) { + nbgl_image_t *image = (nbgl_image_t *) nbgl_objPoolGet(IMAGE, layoutInt->layer); + layoutObj_t *obj + = layoutAddCallbackObj(layoutInt, (nbgl_obj_t *) image, token, TUNE_TAP_CASUAL); + obj->index = index; + image->foregroundColor = BLACK; + image->buffer = &MINI_PUSH_ICON; + image->obj.alignment = RIGHT_TOP; + image->obj.alignmentMarginX = 12; + image->obj.alignTo = (nbgl_obj_t *) textArea; + image->obj.touchMask = (1 << TOUCHED); + image->obj.touchId = VALUE_BUTTON_1_ID + index; + + container->children[container->nbChildren] = (nbgl_obj_t *) image; + container->nbChildren++; + } + } + if (subText != NULL) { + subTextArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); + subTextArea->textColor = BLACK; + subTextArea->text = PIC(subText); + subTextArea->fontId = SMALL_REGULAR_FONT; + subTextArea->style = NO_STYLE; + subTextArea->wrapping = true; + subTextArea->obj.area.width = container->obj.area.width; + subTextArea->obj.area.height = nbgl_getTextHeightInWidth(subTextArea->fontId, + subTextArea->text, + subTextArea->obj.area.width, + subTextArea->wrapping); + subTextArea->textAlignment = MID_LEFT; + subTextArea->obj.alignment = NO_ALIGNMENT; + if (text != NULL) { + subTextArea->obj.alignmentMarginY = TEXT_SUBTEXT_MARGIN; + fullHeight += POST_SUBTEXT_MARGIN; // under the subText + } + else { +#ifdef TARGET_STAX + subTextArea->obj.alignmentMarginY = BORDER_MARGIN; + fullHeight += BORDER_MARGIN; +#else // TARGET_STAX + subTextArea->obj.alignmentMarginY = 26; + fullHeight += 26; // under the subText +#endif // TARGET_STAX + } + container->children[container->nbChildren] = (nbgl_obj_t *) subTextArea; + container->nbChildren++; + fullHeight += subTextArea->obj.area.height + subTextArea->obj.alignmentMarginY; + } + else { + fullHeight += PRE_TEXT_MARGIN; + } + container->obj.area.height = fullHeight; + container->layout = VERTICAL; + container->obj.alignmentMarginX = BORDER_MARGIN; + container->obj.alignment = NO_ALIGNMENT; + // set this new obj as child of main container + layoutAddObject(layoutInt, (nbgl_obj_t *) container); + + return container->obj.area.height; +} + /********************** * GLOBAL FUNCTIONS **********************/ @@ -1187,7 +1290,7 @@ int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switc } /** - * @brief Creates an area with given text and sub text (in gray) + * @brief Creates an area with given text (in bold) and sub text (in regular) * * @param layout the current layout * @param text main text (in small bold font), optional @@ -1196,82 +1299,29 @@ int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switc */ int nbgl_layoutAddText(nbgl_layout_t *layout, const char *text, const char *subText) { - nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *) layout; - nbgl_container_t *container; - nbgl_text_area_t *textArea; - nbgl_text_area_t *subTextArea; - uint16_t fullHeight = 0; - LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutAddText():\n"); - if (layout == NULL) { - return -1; - } - container = (nbgl_container_t *) nbgl_objPoolGet(CONTAINER, layoutInt->layer); - - // get container children - container->children = nbgl_containerPoolGet(2, layoutInt->layer); - container->obj.area.width = AVAILABLE_WIDTH; - - if (text != NULL) { - textArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); - - textArea->textColor = BLACK; - textArea->text = PIC(text); - textArea->textAlignment = MID_LEFT; - textArea->fontId = SMALL_BOLD_FONT; - textArea->style = NO_STYLE; - textArea->wrapping = true; - textArea->obj.alignment = NO_ALIGNMENT; - textArea->obj.alignmentMarginY = PRE_TEXT_MARGIN; - textArea->obj.area.width = container->obj.area.width; - textArea->obj.area.height = nbgl_getTextHeightInWidth( - textArea->fontId, textArea->text, textArea->obj.area.width, textArea->wrapping); - fullHeight += textArea->obj.area.height + textArea->obj.alignmentMarginY; - container->children[container->nbChildren] = (nbgl_obj_t *) textArea; - container->nbChildren++; - } - if (subText != NULL) { - subTextArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); - subTextArea->textColor = BLACK; - subTextArea->text = PIC(subText); - subTextArea->fontId = SMALL_REGULAR_FONT; - subTextArea->style = NO_STYLE; - subTextArea->wrapping = true; - subTextArea->obj.area.width = container->obj.area.width; - subTextArea->obj.area.height = nbgl_getTextHeightInWidth(subTextArea->fontId, - subTextArea->text, - subTextArea->obj.area.width, - subTextArea->wrapping); - subTextArea->textAlignment = MID_LEFT; - subTextArea->obj.alignment = NO_ALIGNMENT; - if (text != NULL) { - subTextArea->obj.alignmentMarginY = TEXT_SUBTEXT_MARGIN; - fullHeight += POST_SUBTEXT_MARGIN; // under the subText - } - else { -#ifdef TARGET_STAX - subTextArea->obj.alignmentMarginY = BORDER_MARGIN; - fullHeight += BORDER_MARGIN; -#else // TARGET_STAX - subTextArea->obj.alignmentMarginY = 26; - fullHeight += 26; // under the subText -#endif // TARGET_STAX - } - container->children[container->nbChildren] = (nbgl_obj_t *) subTextArea; - container->nbChildren++; - fullHeight += subTextArea->obj.area.height + subTextArea->obj.alignmentMarginY; - } - else { - fullHeight += PRE_TEXT_MARGIN; - } - container->obj.area.height = fullHeight; - container->layout = VERTICAL; - container->obj.alignmentMarginX = BORDER_MARGIN; - container->obj.alignment = NO_ALIGNMENT; - // set this new obj as child of main container - layoutAddObject(layoutInt, (nbgl_obj_t *) container); + return addText(layout, text, subText, 0, 0, false); +} - return container->obj.area.height; +/** + * @brief Creates an area with given text (in bold) and sub text (in regular), with a + * > icon on right of text to activate an action when touched, with the given token + * + * @param layout the current layout + * @param text main text (in small bold font), optional + * @param subText description under main text (in small regular font), optional + * @param token token to use in callback when > icon is touched + * @param index index to use in callback when > icon is touched + * @return height of the control if OK + */ +int nbgl_layoutAddTextWithAlias(nbgl_layout_t *layout, + const char *text, + const char *subText, + uint8_t token, + uint8_t index) +{ + LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutAddTextWithAlias():\n"); + return addText(layout, text, subText, token, index, true); } /** diff --git a/lib_nbgl/src/nbgl_page.c b/lib_nbgl/src/nbgl_page.c index 25f0a2a05..f0e037f8b 100644 --- a/lib_nbgl/src/nbgl_page.c +++ b/lib_nbgl/src/nbgl_page.c @@ -199,8 +199,22 @@ static void addContent(nbgl_pageContent_t *content, case INFOS_LIST: { uint8_t i; for (i = 0; i < content->infosList.nbInfos; i++) { - availableHeight -= nbgl_layoutAddText( - layout, content->infosList.infoTypes[i], content->infosList.infoContents[i]); + // if the extension is valid for this index, use a Text with Alias + if ((content->infosList.withExtensions == true) + && (content->infosList.infoExtensions != NULL) + && (content->infosList.infoExtensions[i].fullValue != NULL)) { + availableHeight + -= nbgl_layoutAddTextWithAlias(layout, + content->infosList.infoTypes[i], + content->infosList.infoContents[i], + content->infosList.token, + i); + } + else { + availableHeight -= nbgl_layoutAddText(layout, + content->infosList.infoTypes[i], + content->infosList.infoContents[i]); + } // do not draw a separation line if too low in the container if (availableHeight > 10) { nbgl_layoutAddSeparationLine(layout); diff --git a/lib_nbgl/src/nbgl_use_case.c b/lib_nbgl/src/nbgl_use_case.c index 7a5258ca2..b7151ddb2 100644 --- a/lib_nbgl/src/nbgl_use_case.c +++ b/lib_nbgl/src/nbgl_use_case.c @@ -68,6 +68,7 @@ enum { CONFIRM_TOKEN, REJECT_TOKEN, VALUE_ALIAS_TOKEN, + INFO_ALIAS_TOKEN, BLIND_WARNING_TOKEN, TIP_BOX_TOKEN }; @@ -252,7 +253,9 @@ static char reducedAddress[QRCODE_REDUCED_ADDR_LEN]; **********************/ static void displayReviewPage(uint8_t page, bool forceFullRefresh); static void displayDetailsPage(uint8_t page, bool forceFullRefresh); -static void displayFullValuePage(const nbgl_contentTagValue_t *pair); +static void displayFullValuePage(const char *backText, + const char *aliasText, + const nbgl_contentValueExt_t *extension); static void displayBlindWarning(void); static void displayTipBoxModal(void); static void displaySettingsPage(uint8_t page, bool forceFullRefresh); @@ -444,6 +447,14 @@ static const char *getRejectReviewText(nbgl_operationType_t operationType) static void pageModalCallback(int token, uint8_t index) { LOG_DEBUG(USE_CASE_LOGGER, "pageModalCallback, token = %d, index = %d\n", token, index); + + if (token == INFO_ALIAS_TOKEN) { + // the icon next to info type alias has been touched + displayFullValuePage(tipBoxInfoList.infoTypes[index], + tipBoxInfoList.infoContents[index], + &tipBoxInfoList.infoExtensions[index]); + return; + } nbgl_pageRelease(modalPageContext); modalPageContext = NULL; if (token == NAV_TOKEN) { @@ -580,7 +591,7 @@ static void pageCallback(int token, uint8_t index) else { pair = genericContext.currentCallback(genericContext.currentElementIdx + index); } - displayFullValuePage(pair); + displayFullValuePage(pair->item, pair->value, pair->extension); } else if (token == BLIND_WARNING_TOKEN) { displayBlindWarning(); @@ -1135,7 +1146,9 @@ static void displayDetailsPage(uint8_t detailsPage, bool forceFullRefresh) } // function used to display the content of a full value, when touching an alias of a tag/value pair -static void displayFullValuePage(const nbgl_contentTagValue_t *pair) +static void displayFullValuePage(const char *backText, + const char *aliasText, + const nbgl_contentValueExt_t *extension) { nbgl_layoutDescription_t layoutDescription = {.modal = true, .withLeftBorder = true, @@ -1145,29 +1158,38 @@ static void displayFullValuePage(const nbgl_contentTagValue_t *pair) .separationLine = false, .backAndText.token = 0, .backAndText.tuneId = TUNE_TAP_CASUAL, - .backAndText.text = PIC(pair->item)}; - const char *info; - genericContext.modalLayout = nbgl_layoutGet(&layoutDescription); + .backAndText.text = PIC(backText)}; + genericContext.modalLayout = nbgl_layoutGet(&layoutDescription); // add header with the tag part of the pair, to go back nbgl_layoutAddHeader(genericContext.modalLayout, &headerDesc); - // add full value text - if (pair->extension->explanation == NULL) { - if (pair->extension->aliasType == ENS_ALIAS) { + // add either QR Code or full value text + if (extension->aliasType == QR_CODE_ALIAS) { +#ifdef NBGL_QRCODE + nbgl_layoutQRCode_t qrCode + = {.url = extension->fullValue, + .text1 = (extension->title != NULL) ? extension->title : extension->fullValue, + .text2 = extension->explanation, + .centered = true, + .offsetY = 0}; + + nbgl_layoutAddQRCode(genericContext.modalLayout, &qrCode); +#endif // NBGL_QRCODE + } + else { + const char *info; + // add full value text + if (extension->aliasType == ENS_ALIAS) { info = "ENS names are resolved by Ledger backend."; } - else if (pair->extension->aliasType == ADDRESS_BOOK_ALIAS) { + else if (extension->aliasType == ADDRESS_BOOK_ALIAS) { info = "This account label comes from your Address Book in Ledger Live."; } else { - info = ""; + info = extension->explanation; } + nbgl_layoutAddTextContent( + genericContext.modalLayout, aliasText, extension->fullValue, info); } - else { - info = pair->extension->explanation; - } - nbgl_layoutAddTextContent( - genericContext.modalLayout, pair->value, pair->extension->fullValue, info); - // draw & refresh nbgl_layoutDraw(genericContext.modalLayout); nbgl_refresh(); @@ -1217,13 +1239,16 @@ static void displayTipBoxModal(void) .navWithButtons.quitText = NULL, .progressIndicator = false, .tuneId = TUNE_TAP_CASUAL}; - nbgl_pageContent_t content = {.type = INFOS_LIST, - .topRightIcon = NULL, - .infosList.nbInfos = tipBoxInfoList.nbInfos, - .infosList.infoTypes = tipBoxInfoList.infoTypes, - .infosList.infoContents = tipBoxInfoList.infoContents, - .title = tipBoxModalTitle, - .titleToken = QUIT_TOKEN}; + nbgl_pageContent_t content = {.type = INFOS_LIST, + .topRightIcon = NULL, + .infosList.nbInfos = tipBoxInfoList.nbInfos, + .infosList.withExtensions = tipBoxInfoList.withExtensions, + .infosList.infoTypes = tipBoxInfoList.infoTypes, + .infosList.infoContents = tipBoxInfoList.infoContents, + .infosList.infoExtensions = tipBoxInfoList.infoExtensions, + .infosList.token = INFO_ALIAS_TOKEN, + .title = tipBoxModalTitle, + .titleToken = QUIT_TOKEN}; if (modalPageContext != NULL) { nbgl_pageRelease(modalPageContext); @@ -1770,6 +1795,9 @@ static void useCaseReview(nbgl_operationType_t operationType, prepareReviewFirstPage( &STARTING_CONTENT.content.extendedCenter.contentCenter, icon, reviewTitle, reviewSubTitle); if (tipBox != NULL) { + // do not display "Swipe to review" if a tip-box is displayed + STARTING_CONTENT.content.extendedCenter.contentCenter.subText = NULL; + STARTING_CONTENT.content.extendedCenter.tipBox.icon = tipBox->icon; STARTING_CONTENT.content.extendedCenter.tipBox.text = tipBox->text; STARTING_CONTENT.content.extendedCenter.tipBox.token = TIP_BOX_TOKEN; @@ -1777,7 +1805,11 @@ static void useCaseReview(nbgl_operationType_t operationType, tipBoxModalTitle = tipBox->modalTitle; // the only supported type yet is @ref INFOS_LIST if (tipBox->type == INFOS_LIST) { - memcpy(&tipBoxInfoList, &tipBox->infos, sizeof(nbgl_contentInfoList_t)); + tipBoxInfoList.nbInfos = tipBox->infos.nbInfos; + tipBoxInfoList.withExtensions = tipBox->infos.withExtensions; + tipBoxInfoList.infoTypes = PIC(tipBox->infos.infoTypes); + tipBoxInfoList.infoContents = PIC(tipBox->infos.infoContents); + tipBoxInfoList.infoExtensions = PIC(tipBox->infos.infoExtensions); } }