Переглянути джерело

[FL-1497] GUI textbox element and widget (#792)

* canvas: add font parameters
* elements: add text box element
* widget: add text box element
* nfc: rework delete and info scene with text box widget
* gui: update documentation
* gui: fix canvas_get_font_params return

Co-authored-by: あく <alleteam@gmail.com>
gornekich 4 роки тому
батько
коміт
146cd51894

+ 13 - 0
applications/gui/canvas.c

@@ -6,6 +6,13 @@
 #include <furi-hal.h>
 #include <u8g2_glue.h>
 
+const CanvasFontParameters canvas_font_params[FontTotalNumber] = {
+    [FontPrimary] = {.leading_default = 12, .leading_min = 11, .height = 8, .descender = 2},
+    [FontSecondary] = {.leading_default = 11, .leading_min = 9, .height = 7, .descender = 2},
+    [FontKeyboard] = {.leading_default = 11, .leading_min = 9, .height = 7, .descender = 2},
+    [FontBigNumbers] = {.leading_default = 18, .leading_min = 16, .height = 15, .descender = 0},
+};
+
 Canvas* canvas_init() {
     Canvas* canvas = furi_alloc(sizeof(Canvas));
 
@@ -92,6 +99,12 @@ uint8_t canvas_current_font_height(Canvas* canvas) {
     return font_height;
 }
 
+CanvasFontParameters* canvas_get_font_params(Canvas* canvas, Font font) {
+    furi_assert(canvas);
+    furi_assert(font < FontTotalNumber);
+    return (CanvasFontParameters*)&canvas_font_params[font];
+}
+
 void canvas_clear(Canvas* canvas) {
     furi_assert(canvas);
     u8g2_ClearBuffer(&canvas->fb);

+ 26 - 1
applications/gui/canvas.h

@@ -20,7 +20,15 @@ typedef enum {
 } Color;
 
 /** Fonts enumeration */
-typedef enum { FontPrimary, FontSecondary, FontKeyboard, FontBigNumbers } Font;
+typedef enum {
+    FontPrimary,
+    FontSecondary,
+    FontKeyboard,
+    FontBigNumbers,
+
+    // Keep last for fonts number calculation
+    FontTotalNumber,
+} Font;
 
 /** Alignment enumeration */
 typedef enum {
@@ -45,6 +53,14 @@ typedef enum {
     CanvasFontDirectionDownToTop,
 } CanvasFontDirection;
 
+/** Font parameters */
+typedef struct {
+    uint8_t leading_default;
+    uint8_t leading_min;
+    uint8_t height;
+    uint8_t descender;
+} CanvasFontParameters;
+
 /** Canvas anonymouse structure */
 typedef struct Canvas Canvas;
 
@@ -72,6 +88,15 @@ uint8_t canvas_height(Canvas* canvas);
  */
 uint8_t canvas_current_font_height(Canvas* canvas);
 
+/** Get font parameters
+ *
+ * @param      canvas  Canvas instance
+ * @param      font    Font
+ *
+ * @return     pointer to CanvasFontParameters structure
+ */
+CanvasFontParameters* canvas_get_font_params(Canvas* canvas, Font font);
+
 /** Clear canvas
  *
  * @param      canvas  Canvas instance

+ 215 - 0
applications/gui/elements.c

@@ -10,6 +10,18 @@
 
 #include <string.h>
 #include <stdint.h>
+#include <stdbool.h>
+
+typedef struct {
+    uint8_t x;
+    uint8_t y;
+    uint8_t leading_min;
+    uint8_t leading_default;
+    uint8_t height;
+    uint8_t descender;
+    uint8_t len;
+    const char* text;
+} ElementTextBoxLine;
 
 void elements_progress_bar(
     Canvas* canvas,
@@ -352,3 +364,206 @@ void elements_string_fit_width(Canvas* canvas, string_t string, uint8_t width) {
         string_cat(string, "...");
     }
 }
+
+void elements_text_box(
+    Canvas* canvas,
+    uint8_t x,
+    uint8_t y,
+    uint8_t width,
+    uint8_t height,
+    Align horizontal,
+    Align vertical,
+    const char* text) {
+    furi_assert(canvas);
+
+    ElementTextBoxLine line[ELEMENTS_MAX_LINES_NUM];
+    bool bold = false;
+    bool mono = false;
+    bool inversed = false;
+    bool inversed_present = false;
+    Font current_font = FontSecondary;
+    Font prev_font = FontSecondary;
+    CanvasFontParameters* font_params = canvas_get_font_params(canvas, current_font);
+
+    // Fill line parameters
+    uint8_t line_leading_min = font_params->leading_min;
+    uint8_t line_leading_default = font_params->leading_default;
+    uint8_t line_height = font_params->height;
+    uint8_t line_descender = font_params->descender;
+    uint8_t line_num = 0;
+    uint8_t line_width = 0;
+    uint8_t line_len = 0;
+    uint8_t total_height_min = 0;
+    uint8_t total_height_default = 0;
+    uint16_t i = 0;
+    bool full_text_processed = false;
+
+    canvas_set_font(canvas, FontSecondary);
+
+    // Fill all lines
+    line[0].text = text;
+    for(i = 0; !full_text_processed; i++) {
+        line_len++;
+        // Identify line height
+        if(prev_font != current_font) {
+            font_params = canvas_get_font_params(canvas, current_font);
+            line_leading_min = MAX(line_leading_min, font_params->leading_min);
+            line_leading_default = MAX(line_leading_default, font_params->leading_default);
+            line_height = MAX(line_height, font_params->height);
+            line_descender = MAX(line_descender, font_params->descender);
+            prev_font = current_font;
+        }
+        // Set the font
+        if(text[i] == '\e' && text[i + 1]) {
+            i++;
+            line_len++;
+            if(text[i] == ELEMENTS_BOLD_MARKER) {
+                if(bold) {
+                    current_font = FontSecondary;
+                } else {
+                    current_font = FontPrimary;
+                }
+                canvas_set_font(canvas, current_font);
+                bold = !bold;
+            }
+            if(text[i] == ELEMENTS_MONO_MARKER) {
+                if(mono) {
+                    current_font = FontSecondary;
+                } else {
+                    current_font = FontKeyboard;
+                }
+                canvas_set_font(canvas, FontKeyboard);
+                mono = !mono;
+            }
+            if(text[i] == ELEMENTS_INVERSED_MARKER) {
+                inversed_present = true;
+            }
+            continue;
+        }
+        if(text[i] != '\n') {
+            line_width += canvas_glyph_width(canvas, text[i]);
+        }
+        // Process new line
+        if(text[i] == '\n' || text[i] == '\0' || line_width > width) {
+            if(line_width > width) {
+                line_width -= canvas_glyph_width(canvas, text[i--]);
+                line_len--;
+            }
+            if(text[i] == '\0') {
+                full_text_processed = true;
+            }
+            if(inversed_present) {
+                line_leading_min += 1;
+                line_leading_default += 1;
+                inversed_present = false;
+            }
+            line[line_num].leading_min = line_leading_min;
+            line[line_num].leading_default = line_leading_default;
+            line[line_num].height = line_height;
+            line[line_num].descender = line_descender;
+            if(total_height_min + line_leading_min > height) {
+                line_num--;
+                break;
+            }
+            total_height_min += line_leading_min;
+            total_height_default += line_leading_default;
+            line[line_num].len = line_len;
+            if(horizontal == AlignCenter) {
+                line[line_num].x = x + (width - line_width) / 2;
+            } else if(horizontal == AlignRight) {
+                line[line_num].x = x + (width - line_width);
+            } else {
+                line[line_num].x = x;
+            }
+            line[line_num].y = total_height_min;
+            line_num++;
+            if(text[i + 1]) {
+                line[line_num].text = &text[i + 1];
+            }
+
+            line_leading_min = font_params->leading_min;
+            line_height = font_params->height;
+            line_descender = font_params->descender;
+            line_width = 0;
+            line_len = 0;
+        }
+    }
+
+    // Set vertical alignment for all lines
+    if(full_text_processed) {
+        if(total_height_default < height) {
+            if(vertical == AlignTop) {
+                line[0].y = y + line[0].height;
+            } else if(vertical == AlignCenter) {
+                line[0].y = y + line[0].height + (height - total_height_default) / 2;
+            } else if(vertical == AlignBottom) {
+                line[0].y = y + line[0].height + (height - total_height_default);
+            }
+            if(line_num > 1) {
+                for(uint8_t i = 1; i < line_num; i++) {
+                    line[i].y = line[i - 1].y + line[i - 1].leading_default;
+                }
+            }
+        } else if(line_num > 1) {
+            uint8_t free_pixel_num = height - total_height_min;
+            uint8_t fill_pixel = 0;
+            uint8_t j = 1;
+            line[0].y = line[0].height;
+            while(fill_pixel < free_pixel_num) {
+                line[j].y = line[j - 1].y + line[j - 1].leading_min + 1;
+                fill_pixel++;
+                j = j % (line_num - 1) + 1;
+            }
+        }
+    }
+
+    // Draw line by line
+    canvas_set_font(canvas, FontSecondary);
+    bold = false;
+    mono = false;
+    inversed = false;
+    for(uint8_t i = 0; i < line_num; i++) {
+        for(uint8_t j = 0; j < line[i].len; j++) {
+            // Process format symbols
+            if(line[i].text[j] == ELEMENTS_BOLD_MARKER) {
+                if(bold) {
+                    current_font = FontSecondary;
+                } else {
+                    current_font = FontPrimary;
+                }
+                canvas_set_font(canvas, current_font);
+                bold = !bold;
+                continue;
+            }
+            if(line[i].text[j] == ELEMENTS_MONO_MARKER) {
+                if(mono) {
+                    current_font = FontSecondary;
+                } else {
+                    current_font = FontKeyboard;
+                }
+                canvas_set_font(canvas, current_font);
+                mono = !mono;
+                continue;
+            }
+            if(line[i].text[j] == ELEMENTS_INVERSED_MARKER) {
+                inversed = !inversed;
+                continue;
+            }
+            if(inversed) {
+                canvas_draw_box(
+                    canvas,
+                    line[i].x - 1,
+                    line[i].y - line[i].height - 1,
+                    canvas_glyph_width(canvas, line[i].text[j]) + 1,
+                    line[i].height + line[i].descender + 2);
+                canvas_invert_color(canvas);
+                canvas_draw_glyph(canvas, line[i].x, line[i].y, line[i].text[j]);
+                canvas_invert_color(canvas);
+            } else {
+                canvas_draw_glyph(canvas, line[i].x, line[i].y, line[i].text[j]);
+            }
+            line[i].x += canvas_glyph_width(canvas, line[i].text[j]);
+        }
+    }
+    canvas_set_font(canvas, FontSecondary);
+}

+ 92 - 35
applications/gui/elements.h

@@ -16,12 +16,19 @@
 extern "C" {
 #endif
 
+#define ELEMENTS_MAX_LINES_NUM (7)
+#define ELEMENTS_BOLD_MARKER '#'
+#define ELEMENTS_MONO_MARKER '*'
+#define ELEMENTS_INVERSED_MARKER '!'
+
 /** Draw progress bar.
- * @param x - progress bar position on X axis
- * @param y - progress bar position on Y axis
- * @param width - progress bar width
- * @param progress - progress in unnamed metric
- * @param total - total amount in unnamed metric
+ *
+ * @param   canvas      Canvas instance
+ * @param   x           progress bar position on X axis
+ * @param   y           progress bar position on Y axis
+ * @param   width       progress bar width
+ * @param   progress    progress in unnamed metric
+ * @param   total       total amount in unnamed metric
  */
 void elements_progress_bar(
     Canvas* canvas,
@@ -32,11 +39,13 @@ void elements_progress_bar(
     uint8_t total);
 
 /** Draw scrollbar on canvas at specific position.
- * @param x - scrollbar position on X axis
- * @param y - scrollbar position on Y axis
- * @param height - scrollbar height
- * @param pos - current element 
- * @param total - total elements
+ *
+ * @param   canvas  Canvas instance
+ * @param   x       scrollbar position on X axis
+ * @param   y       scrollbar position on Y axis
+ * @param   height  scrollbar height
+ * @param   pos     current element
+ * @param   total   total elements
  */
 void elements_scrollbar_pos(
     Canvas* canvas,
@@ -47,37 +56,49 @@ void elements_scrollbar_pos(
     uint16_t total);
 
 /** Draw scrollbar on canvas.
- * width 3px, height equal to canvas height
- * @param pos - current element of total elements
- * @param total - total elements
+ * @note    width 3px, height equal to canvas height
+ *
+ * @param   canvas  Canvas instance
+ * @param   pos     current element of total elements
+ * @param   total   total elements
  */
 void elements_scrollbar(Canvas* canvas, uint16_t pos, uint16_t total);
 
 /** Draw rounded frame
- * @param x, y - top left corner coordinates
- * @param width, height - frame width and height
+ *
+ * @param   canvas          Canvas instance
+ * @param   x, y            top left corner coordinates
+ * @param   width, height   frame width and height
  */
 void elements_frame(Canvas* canvas, uint8_t x, uint8_t y, uint8_t width, uint8_t height);
 
 /** Draw button in left corner
- * @param str - button text
+ *
+ * @param   canvas  Canvas instance
+ * @param   str     button text
  */
 void elements_button_left(Canvas* canvas, const char* str);
 
 /** Draw button in right corner
- * @param str - button text
+ *
+ * @param   canvas  Canvas instance
+ * @param   str     button text
  */
 void elements_button_right(Canvas* canvas, const char* str);
 
 /** Draw button in center
- * @param str - button text
+ *
+ * @param   canvas  Canvas instance
+ * @param   str     button text
  */
 void elements_button_center(Canvas* canvas, const char* str);
 
 /** Draw aligned multiline text
- * @param x, y - coordinates based on align param
- * @param horizontal, vertical - aligment of multiline text
- * @param text - string (possible multiline)
+ *
+ * @param   canvas                  Canvas instance
+ * @param   x, y                    coordinates based on align param
+ * @param   horizontal, vertical    aligment of multiline text
+ * @param   text                    string (possible multiline)
  */
 void elements_multiline_text_aligned(
     Canvas* canvas,
@@ -88,20 +109,26 @@ void elements_multiline_text_aligned(
     const char* text);
 
 /** Draw multiline text
- * @param x, y - top left corner coordinates
- * @param text - string (possible multiline)
+ *
+ * @param   canvas  Canvas instance
+ * @param   x, y    top left corner coordinates
+ * @param   text    string (possible multiline)
  */
 void elements_multiline_text(Canvas* canvas, uint8_t x, uint8_t y, const char* text);
 
 /** Draw framed multiline text
- * @param x, y - top left corner coordinates
- * @param text - string (possible multiline)
+ *
+ * @param   canvas  Canvas instance
+ * @param   x, y    top left corner coordinates
+ * @param   text    string (possible multiline)
  */
 void elements_multiline_text_framed(Canvas* canvas, uint8_t x, uint8_t y, const char* text);
 
 /** Draw slightly rounded frame
- * @param x, y - top left corner coordinates
- * @param width, height - size of frame
+ *
+ * @param   canvas          Canvas instance
+ * @param   x, y            top left corner coordinates
+ * @param   width, height   size of frame
  */
 void elements_slightly_rounded_frame(
     Canvas* canvas,
@@ -111,8 +138,10 @@ void elements_slightly_rounded_frame(
     uint8_t height);
 
 /** Draw slightly rounded box
- * @param x, y - top left corner coordinates
- * @param width, height - size of box
+ *
+ * @param   canvas          Canvas instance
+ * @param   x, y            top left corner coordinates
+ * @param   width, height   size of box
  */
 void elements_slightly_rounded_box(
     Canvas* canvas,
@@ -122,19 +151,47 @@ void elements_slightly_rounded_box(
     uint8_t height);
 
 /** Draw bubble frame for text
- * @param x - left x coordinates
- * @param y - top y coordinate
- * @param width - bubble width
- * @param height - bubble height
+ *
+ * @param   canvas  Canvas instance
+ * @param   x       left x coordinates
+ * @param   y       top y coordinate
+ * @param   width   bubble width
+ * @param   height  bubble height
  */
 void elements_bubble(Canvas* canvas, uint8_t x, uint8_t y, uint8_t width, uint8_t height);
 
 /** Trim string buffer to fit width in pixels
- * @param string - string to trim
- * @param width - max width
+ *
+ * @param   canvas  Canvas instance
+ * @param   string  string to trim
+ * @param   width   max width
  */
 void elements_string_fit_width(Canvas* canvas, string_t string, uint8_t width);
 
+/** Draw text box element
+ *
+ * @param       canvas      Canvas instance
+ * @param       x           x coordinate
+ * @param       y           y coordinate
+ * @param       width       width to fit text
+ * @param       height      height to fit text
+ * @param       horizontal  Align instance
+ * @param       vertical    Align instance
+ * @param[in]   text        Formatted text. The following formats are available:
+ *                          "\e#Bold text\e#" - bold font is used
+ *                          "\e*Monospaced text\e*" - monospaced font is used
+ *                          "\e#Inversed text\e#" - white text on black background
+ */
+void elements_text_box(
+    Canvas* canvas,
+    uint8_t x,
+    uint8_t y,
+    uint8_t width,
+    uint8_t height,
+    Align horizontal,
+    Align vertical,
+    const char* text);
+
 #ifdef __cplusplus
 }
 #endif

+ 15 - 0
applications/gui/modules/widget.c

@@ -146,6 +146,21 @@ void widget_add_string_element(
     widget_add_element(widget, string_element);
 }
 
+void widget_add_text_box_element(
+    Widget* widget,
+    uint8_t x,
+    uint8_t y,
+    uint8_t width,
+    uint8_t height,
+    Align horizontal,
+    Align vertical,
+    const char* text) {
+    furi_assert(widget);
+    WidgetElement* text_box_element =
+        widget_element_text_box_create(x, y, width, height, horizontal, vertical, text);
+    widget_add_element(widget, text_box_element);
+}
+
 void widget_add_button_element(
     Widget* widget,
     GuiButtonType button_type,

+ 24 - 0
applications/gui/modules/widget.h

@@ -75,6 +75,30 @@ void widget_add_string_element(
     Font font,
     const char* text);
 
+/** Add Text Box Element
+ *
+ * @param      widget      Widget instance
+ * @param      x           x coordinate
+ * @param      y           y coordinate
+ * @param      width       width to fit text
+ * @param      height      height to fit text
+ * @param      horizontal  Align instance
+ * @param      vertical    Align instance
+ * @param[in]  text        Formatted text. The following formats are available:
+ *                          "\e#Bold text\e#" - bold font is used
+ *                          "\e*Monospaced text\e*" - monospaced font is used
+ *                          "\e#Inversed text\e#" - white text on black background
+ */
+void widget_add_text_box_element(
+    Widget* widget,
+    uint8_t x,
+    uint8_t y,
+    uint8_t width,
+    uint8_t height,
+    Align horizontal,
+    Align vertical,
+    const char* text);
+
 /** Add Button Element
  *
  * @param      widget       Widget instance

+ 10 - 0
applications/gui/modules/widget_elements/widget_element_i.h

@@ -52,6 +52,16 @@ WidgetElement* widget_element_string_create(
     Font font,
     const char* text);
 
+/** Create text box element */
+WidgetElement* widget_element_text_box_create(
+    uint8_t x,
+    uint8_t y,
+    uint8_t width,
+    uint8_t height,
+    Align horizontal,
+    Align vertical,
+    const char* text);
+
 /** Create button element */
 WidgetElement* widget_element_button_create(
     GuiButtonType button_type,

+ 71 - 0
applications/gui/modules/widget_elements/widget_element_text_box.c

@@ -0,0 +1,71 @@
+#include "widget_element_i.h"
+#include <m-string.h>
+#include <gui/elements.h>
+
+typedef struct {
+    uint8_t x;
+    uint8_t y;
+    uint8_t width;
+    uint8_t height;
+    Align horizontal;
+    Align vertical;
+    string_t text;
+} GuiTextBoxModel;
+
+static void gui_text_box_draw(Canvas* canvas, WidgetElement* element) {
+    furi_assert(canvas);
+    furi_assert(element);
+    GuiTextBoxModel* model = element->model;
+
+    if(string_size(model->text)) {
+        elements_text_box(
+            canvas,
+            model->x,
+            model->y,
+            model->width,
+            model->height,
+            model->horizontal,
+            model->vertical,
+            string_get_cstr(model->text));
+    }
+}
+
+static void gui_text_box_free(WidgetElement* gui_string) {
+    furi_assert(gui_string);
+
+    GuiTextBoxModel* model = gui_string->model;
+    string_clear(model->text);
+    free(gui_string->model);
+    free(gui_string);
+}
+
+WidgetElement* widget_element_text_box_create(
+    uint8_t x,
+    uint8_t y,
+    uint8_t width,
+    uint8_t height,
+    Align horizontal,
+    Align vertical,
+    const char* text) {
+    furi_assert(text);
+
+    // Allocate and init model
+    GuiTextBoxModel* model = furi_alloc(sizeof(GuiTextBoxModel));
+    model->x = x;
+    model->y = y;
+    model->width = width;
+    model->height = height;
+    model->horizontal = horizontal;
+    model->vertical = vertical;
+    string_init_set_str(model->text, text);
+
+    // Allocate and init Element
+    WidgetElement* gui_string = furi_alloc(sizeof(WidgetElement));
+    gui_string->parent = NULL;
+    gui_string->input = NULL;
+    gui_string->draw = gui_text_box_draw;
+    gui_string->free = gui_text_box_free;
+    gui_string->model = model;
+
+    return gui_string;
+}

+ 2 - 2
applications/nfc/scenes/nfc_scene_delete.c

@@ -12,8 +12,8 @@ void nfc_scene_delete_on_enter(void* context) {
 
     // Setup Custom Widget view
     char delete_str[64];
-    snprintf(delete_str, sizeof(delete_str), "Delete %s", nfc->dev.dev_name);
-    widget_add_string_element(nfc->widget, 64, 6, AlignCenter, AlignTop, FontPrimary, delete_str);
+    snprintf(delete_str, sizeof(delete_str), "\e#Delete %s\e#", nfc->dev.dev_name);
+    widget_add_text_box_element(nfc->widget, 0, 0, 128, 24, AlignCenter, AlignCenter, delete_str);
     widget_add_button_element(
         nfc->widget, GuiButtonTypeLeft, "Back", nfc_scene_delete_widget_callback, nfc);
     widget_add_button_element(

+ 2 - 2
applications/nfc/scenes/nfc_scene_device_info.c

@@ -35,8 +35,8 @@ void nfc_scene_device_info_on_enter(void* context) {
     Nfc* nfc = context;
 
     // Setup Custom Widget view
-    widget_add_string_element(
-        nfc->widget, 64, 6, AlignCenter, AlignTop, FontSecondary, nfc->dev.dev_name);
+    widget_add_text_box_element(
+        nfc->widget, 0, 0, 128, 24, AlignCenter, AlignCenter, nfc->dev.dev_name);
     widget_add_button_element(
         nfc->widget, GuiButtonTypeLeft, "Back", nfc_scene_device_info_widget_callback, nfc);
     widget_add_button_element(