[FL-2627] Flipper applications: SDK, build and debug system (#1387)

* Added support for running applications from SD card (FAPs - Flipper Application Packages)
* Added plugin_dist target for fbt to build FAPs
* All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default
* Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them
* Added debugging support for FAPs with fbt debug & VSCode
* Added public firmware API with automated versioning

Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: SG <who.just.the.doctor@gmail.com>
Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
SG
2022-09-15 02:11:38 +10:00
committed by Aleksandr Kutuzov
parent 0f6f9ad52e
commit b9a766d909
895 changed files with 8862 additions and 1465 deletions

View File

@@ -0,0 +1,343 @@
#include "button_menu.h"
#include "gui/canvas.h"
#include "gui/elements.h"
#include "input/input.h"
#include <m-array.h>
#include <furi.h>
#include <stdint.h>
#define ITEM_FIRST_OFFSET 17
#define ITEM_NEXT_OFFSET 4
#define ITEM_HEIGHT 14
#define ITEM_WIDTH 64
#define BUTTONS_PER_SCREEN 6
struct ButtonMenuItem {
const char* label;
int32_t index;
ButtonMenuItemCallback callback;
ButtonMenuItemType type;
void* callback_context;
};
ARRAY_DEF(ButtonMenuItemArray, ButtonMenuItem, M_POD_OPLIST);
struct ButtonMenu {
View* view;
bool freeze_input;
};
typedef struct {
ButtonMenuItemArray_t items;
uint8_t position;
const char* header;
} ButtonMenuModel;
static void button_menu_draw_control_button(
Canvas* canvas,
uint8_t item_position,
const char* text,
bool selected) {
furi_assert(canvas);
furi_assert(text);
uint8_t item_x = 0;
uint8_t item_y = ITEM_FIRST_OFFSET + (item_position * (ITEM_HEIGHT + ITEM_NEXT_OFFSET));
canvas_set_color(canvas, ColorBlack);
if(selected) {
elements_slightly_rounded_box(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT);
canvas_set_color(canvas, ColorWhite);
}
canvas_draw_str_aligned(
canvas,
item_x + (ITEM_WIDTH / 2),
item_y + (ITEM_HEIGHT / 2),
AlignCenter,
AlignCenter,
text);
}
static void button_menu_draw_common_button(
Canvas* canvas,
uint8_t item_position,
const char* text,
bool selected) {
furi_assert(canvas);
furi_assert(text);
uint8_t item_x = 0;
uint8_t item_y = ITEM_FIRST_OFFSET + (item_position * (ITEM_HEIGHT + ITEM_NEXT_OFFSET));
canvas_set_color(canvas, ColorBlack);
if(selected) {
canvas_draw_rbox(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 5);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_draw_rframe(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 5);
}
string_t disp_str;
string_init_set_str(disp_str, text);
elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6);
canvas_draw_str_aligned(
canvas,
item_x + (ITEM_WIDTH / 2),
item_y + (ITEM_HEIGHT / 2),
AlignCenter,
AlignCenter,
string_get_cstr(disp_str));
string_clear(disp_str);
}
static void button_menu_view_draw_callback(Canvas* canvas, void* _model) {
furi_assert(canvas);
furi_assert(_model);
ButtonMenuModel* model = (ButtonMenuModel*)_model;
canvas_set_font(canvas, FontSecondary);
uint8_t item_position = 0;
int8_t active_screen = model->position / BUTTONS_PER_SCREEN;
size_t items_size = ButtonMenuItemArray_size(model->items);
int8_t max_screen = ((int16_t)items_size - 1) / BUTTONS_PER_SCREEN;
ButtonMenuItemArray_it_t it;
if(active_screen > 0) {
canvas_draw_icon(canvas, 28, 1, &I_InfraredArrowUp_4x8);
}
if(max_screen > active_screen) {
canvas_draw_icon(canvas, 28, 123, &I_InfraredArrowDown_4x8);
}
if(model->header) {
string_t disp_str;
string_init_set_str(disp_str, model->header);
elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6);
canvas_draw_str_aligned(
canvas, 32, 10, AlignCenter, AlignCenter, string_get_cstr(disp_str));
string_clear(disp_str);
}
for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
ButtonMenuItemArray_next(it), ++item_position) {
if(active_screen == (item_position / BUTTONS_PER_SCREEN)) {
if(ButtonMenuItemArray_cref(it)->type == ButtonMenuItemTypeControl) {
button_menu_draw_control_button(
canvas,
item_position % BUTTONS_PER_SCREEN,
ButtonMenuItemArray_cref(it)->label,
(item_position == model->position));
} else if(ButtonMenuItemArray_cref(it)->type == ButtonMenuItemTypeCommon) {
button_menu_draw_common_button(
canvas,
item_position % BUTTONS_PER_SCREEN,
ButtonMenuItemArray_cref(it)->label,
(item_position == model->position));
}
}
}
}
static void button_menu_process_up(ButtonMenu* button_menu) {
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
if(model->position > 0) {
model->position--;
} else {
model->position = ButtonMenuItemArray_size(model->items) - 1;
}
return true;
});
}
static void button_menu_process_down(ButtonMenu* button_menu) {
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
if(model->position < (ButtonMenuItemArray_size(model->items) - 1)) {
model->position++;
} else {
model->position = 0;
}
return true;
});
}
static void button_menu_process_ok(ButtonMenu* button_menu, InputType type) {
furi_assert(button_menu);
ButtonMenuItem* item = NULL;
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
if(model->position < (ButtonMenuItemArray_size(model->items))) {
item = ButtonMenuItemArray_get(model->items, model->position);
}
return false;
});
if(item) {
if(item->type == ButtonMenuItemTypeControl) {
if(type == InputTypeShort) {
if(item && item->callback) {
item->callback(item->callback_context, item->index, type);
}
}
}
if(item->type == ButtonMenuItemTypeCommon) {
if((type == InputTypePress) || (type == InputTypeRelease)) {
if(item && item->callback) {
item->callback(item->callback_context, item->index, type);
}
}
}
}
}
static bool button_menu_view_input_callback(InputEvent* event, void* context) {
furi_assert(event);
ButtonMenu* button_menu = context;
bool consumed = false;
if(event->key == InputKeyOk) {
if((event->type == InputTypeRelease) || (event->type == InputTypePress)) {
consumed = true;
button_menu->freeze_input = (event->type == InputTypePress);
button_menu_process_ok(button_menu, event->type);
} else if(event->type == InputTypeShort) {
consumed = true;
button_menu_process_ok(button_menu, event->type);
}
}
if(!button_menu->freeze_input &&
((event->type == InputTypeRepeat) || (event->type == InputTypeShort))) {
switch(event->key) {
case InputKeyUp:
consumed = true;
button_menu_process_up(button_menu);
break;
case InputKeyDown:
consumed = true;
button_menu_process_down(button_menu);
break;
default:
break;
}
}
return consumed;
}
View* button_menu_get_view(ButtonMenu* button_menu) {
furi_assert(button_menu);
return button_menu->view;
}
void button_menu_reset(ButtonMenu* button_menu) {
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
ButtonMenuItemArray_reset(model->items);
model->position = 0;
model->header = NULL;
return true;
});
}
void button_menu_set_header(ButtonMenu* button_menu, const char* header) {
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
model->header = header;
return true;
});
}
ButtonMenuItem* button_menu_add_item(
ButtonMenu* button_menu,
const char* label,
int32_t index,
ButtonMenuItemCallback callback,
ButtonMenuItemType type,
void* callback_context) {
ButtonMenuItem* item = NULL;
furi_assert(label);
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
item = ButtonMenuItemArray_push_new(model->items);
item->label = label;
item->index = index;
item->type = type;
item->callback = callback;
item->callback_context = callback_context;
return true;
});
return item;
}
ButtonMenu* button_menu_alloc(void) {
ButtonMenu* button_menu = malloc(sizeof(ButtonMenu));
button_menu->view = view_alloc();
view_set_orientation(button_menu->view, ViewOrientationVertical);
view_set_context(button_menu->view, button_menu);
view_allocate_model(button_menu->view, ViewModelTypeLocking, sizeof(ButtonMenuModel));
view_set_draw_callback(button_menu->view, button_menu_view_draw_callback);
view_set_input_callback(button_menu->view, button_menu_view_input_callback);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
ButtonMenuItemArray_init(model->items);
model->position = 0;
model->header = NULL;
return true;
});
button_menu->freeze_input = false;
return button_menu;
}
void button_menu_free(ButtonMenu* button_menu) {
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
ButtonMenuItemArray_clear(model->items);
return true;
});
view_free(button_menu->view);
free(button_menu);
}
void button_menu_set_selected_item(ButtonMenu* button_menu, uint32_t index) {
furi_assert(button_menu);
with_view_model(
button_menu->view, (ButtonMenuModel * model) {
uint8_t item_position = 0;
ButtonMenuItemArray_it_t it;
for(ButtonMenuItemArray_it(it, model->items); !ButtonMenuItemArray_end_p(it);
ButtonMenuItemArray_next(it), ++item_position) {
if((uint32_t)ButtonMenuItemArray_cref(it)->index == index) {
model->position = item_position;
break;
}
}
return true;
});
}

View File

@@ -0,0 +1,94 @@
/**
* @file button_menu.h
* GUI: ButtonMenu view module API
*/
#pragma once
#include <stdint.h>
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** ButtonMenu anonymous structure */
typedef struct ButtonMenu ButtonMenu;
/** ButtonMenuItem anonymous structure */
typedef struct ButtonMenuItem ButtonMenuItem;
/** Callback for any button menu actions */
typedef void (*ButtonMenuItemCallback)(void* context, int32_t index, InputType type);
/** Type of button. Difference in drawing buttons. */
typedef enum {
ButtonMenuItemTypeCommon,
ButtonMenuItemTypeControl,
} ButtonMenuItemType;
/** Get button menu view
*
* @param button_menu ButtonMenu instance
*
* @return View instance that can be used for embedding
*/
View* button_menu_get_view(ButtonMenu* button_menu);
/** Clean button menu
*
* @param button_menu ButtonMenu instance
*/
void button_menu_reset(ButtonMenu* button_menu);
/** Add item to button menu instance
*
* @param button_menu ButtonMenu instance
* @param label text inside new button
* @param index value to distinct between buttons inside
* ButtonMenuItemCallback
* @param callback The callback
* @param type type of button to create. Differ by button
* drawing. Control buttons have no frames, and
* have more squared borders.
* @param callback_context The callback context
*
* @return pointer to just-created item
*/
ButtonMenuItem* button_menu_add_item(
ButtonMenu* button_menu,
const char* label,
int32_t index,
ButtonMenuItemCallback callback,
ButtonMenuItemType type,
void* callback_context);
/** Allocate and initialize new instance of ButtonMenu model
*
* @return just-created ButtonMenu model
*/
ButtonMenu* button_menu_alloc(void);
/** Free ButtonMenu element
*
* @param button_menu ButtonMenu instance
*/
void button_menu_free(ButtonMenu* button_menu);
/** Set ButtonMenu header on top of canvas
*
* @param button_menu ButtonMenu instance
* @param header header on the top of button menu
*/
void button_menu_set_header(ButtonMenu* button_menu, const char* header);
/** Set selected item
*
* @param button_menu ButtonMenu instance
* @param index index of ButtonMenu to be selected
*/
void button_menu_set_selected_item(ButtonMenu* button_menu, uint32_t index);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,383 @@
#include "button_panel.h"
#include "furi_hal_resources.h"
#include "gui/canvas.h"
#include <m-array.h>
#include <m-i-list.h>
#include <m-list.h>
#include <furi.h>
#include <gui/elements.h>
#include <stdint.h>
typedef struct {
// uint16_t to support multi-screen, wide button panel
uint16_t x;
uint16_t y;
Font font;
const char* str;
} LabelElement;
LIST_DEF(LabelList, LabelElement, M_POD_OPLIST)
#define M_OPL_LabelList_t() LIST_OPLIST(LabelList)
typedef struct {
uint16_t x;
uint16_t y;
const Icon* name;
const Icon* name_selected;
} IconElement;
typedef struct ButtonItem {
uint32_t index;
ButtonItemCallback callback;
IconElement icon;
void* callback_context;
} ButtonItem;
ARRAY_DEF(ButtonArray, ButtonItem*, M_PTR_OPLIST);
#define M_OPL_ButtonArray_t() ARRAY_OPLIST(ButtonArray, M_PTR_OPLIST)
ARRAY_DEF(ButtonMatrix, ButtonArray_t);
#define M_OPL_ButtonMatrix_t() ARRAY_OPLIST(ButtonMatrix, M_OPL_ButtonArray_t())
struct ButtonPanel {
View* view;
};
typedef struct {
ButtonMatrix_t button_matrix;
LabelList_t labels;
uint16_t reserve_x;
uint16_t reserve_y;
uint16_t selected_item_x;
uint16_t selected_item_y;
} ButtonPanelModel;
static ButtonItem** button_panel_get_item(ButtonPanelModel* model, size_t x, size_t y);
static void button_panel_process_up(ButtonPanel* button_panel);
static void button_panel_process_down(ButtonPanel* button_panel);
static void button_panel_process_left(ButtonPanel* button_panel);
static void button_panel_process_right(ButtonPanel* button_panel);
static void button_panel_process_ok(ButtonPanel* button_panel);
static void button_panel_view_draw_callback(Canvas* canvas, void* _model);
static bool button_panel_view_input_callback(InputEvent* event, void* context);
ButtonPanel* button_panel_alloc() {
ButtonPanel* button_panel = malloc(sizeof(ButtonPanel));
button_panel->view = view_alloc();
view_set_orientation(button_panel->view, ViewOrientationVertical);
view_set_context(button_panel->view, button_panel);
view_allocate_model(button_panel->view, ViewModelTypeLocking, sizeof(ButtonPanelModel));
view_set_draw_callback(button_panel->view, button_panel_view_draw_callback);
view_set_input_callback(button_panel->view, button_panel_view_input_callback);
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
model->reserve_x = 0;
model->reserve_y = 0;
model->selected_item_x = 0;
model->selected_item_y = 0;
ButtonMatrix_init(model->button_matrix);
LabelList_init(model->labels);
return true;
});
return button_panel;
}
void button_panel_reserve(ButtonPanel* button_panel, size_t reserve_x, size_t reserve_y) {
furi_check(reserve_x > 0);
furi_check(reserve_y > 0);
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
model->reserve_x = reserve_x;
model->reserve_y = reserve_y;
ButtonMatrix_reserve(model->button_matrix, model->reserve_y);
for(size_t i = 0; i > model->reserve_y; ++i) {
ButtonArray_t* array = ButtonMatrix_get(model->button_matrix, i);
ButtonArray_init(*array);
ButtonArray_reserve(*array, reserve_x);
// TODO: do we need to clear allocated memory of ptr-s to ButtonItem ??
}
LabelList_init(model->labels);
return true;
});
}
void button_panel_free(ButtonPanel* button_panel) {
furi_assert(button_panel);
button_panel_reset(button_panel);
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
LabelList_clear(model->labels);
ButtonMatrix_clear(model->button_matrix);
return true;
});
view_free(button_panel->view);
free(button_panel);
}
void button_panel_reset(ButtonPanel* button_panel) {
furi_assert(button_panel);
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
for(size_t x = 0; x < model->reserve_x; ++x) {
for(size_t y = 0; y < model->reserve_y; ++y) {
ButtonItem** button_item = button_panel_get_item(model, x, y);
free(*button_item);
*button_item = NULL;
}
}
model->reserve_x = 0;
model->reserve_y = 0;
LabelList_reset(model->labels);
ButtonMatrix_reset(model->button_matrix);
return true;
});
}
static ButtonItem** button_panel_get_item(ButtonPanelModel* model, size_t x, size_t y) {
furi_assert(model);
furi_check(x < model->reserve_x);
furi_check(y < model->reserve_y);
ButtonArray_t* button_array = ButtonMatrix_safe_get(model->button_matrix, x);
ButtonItem** button_item = ButtonArray_safe_get(*button_array, y);
return button_item;
}
void button_panel_add_item(
ButtonPanel* button_panel,
uint32_t index,
uint16_t matrix_place_x,
uint16_t matrix_place_y,
uint16_t x,
uint16_t y,
const Icon* icon_name,
const Icon* icon_name_selected,
ButtonItemCallback callback,
void* callback_context) {
furi_assert(button_panel);
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
ButtonItem** button_item_ptr =
button_panel_get_item(model, matrix_place_x, matrix_place_y);
furi_check(*button_item_ptr == NULL);
*button_item_ptr = malloc(sizeof(ButtonItem));
ButtonItem* button_item = *button_item_ptr;
button_item->callback = callback;
button_item->callback_context = callback_context;
button_item->icon.x = x;
button_item->icon.y = y;
button_item->icon.name = icon_name;
button_item->icon.name_selected = icon_name_selected;
button_item->index = index;
return true;
});
}
View* button_panel_get_view(ButtonPanel* button_panel) {
furi_assert(button_panel);
return button_panel->view;
}
static void button_panel_view_draw_callback(Canvas* canvas, void* _model) {
furi_assert(canvas);
furi_assert(_model);
ButtonPanelModel* model = _model;
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
for(size_t x = 0; x < model->reserve_x; ++x) {
for(size_t y = 0; y < model->reserve_y; ++y) {
ButtonItem* button_item = *button_panel_get_item(model, x, y);
const Icon* icon_name = button_item->icon.name;
if((model->selected_item_x == x) && (model->selected_item_y == y)) {
icon_name = button_item->icon.name_selected;
}
canvas_draw_icon(canvas, button_item->icon.x, button_item->icon.y, icon_name);
}
}
for
M_EACH(label, model->labels, LabelList_t) {
canvas_set_font(canvas, label->font);
canvas_draw_str(canvas, label->x, label->y, label->str);
}
}
static void button_panel_process_down(ButtonPanel* button_panel) {
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
uint16_t new_selected_item_x = model->selected_item_x;
uint16_t new_selected_item_y = model->selected_item_y;
size_t i;
if(new_selected_item_y >= (model->reserve_y - 1)) return false;
++new_selected_item_y;
for(i = 0; i < model->reserve_x; ++i) {
new_selected_item_x = (model->selected_item_x + i) % model->reserve_x;
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
break;
}
}
if(i == model->reserve_x) return false;
model->selected_item_x = new_selected_item_x;
model->selected_item_y = new_selected_item_y;
return true;
});
}
static void button_panel_process_up(ButtonPanel* button_panel) {
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
size_t new_selected_item_x = model->selected_item_x;
size_t new_selected_item_y = model->selected_item_y;
size_t i;
if(new_selected_item_y <= 0) return false;
--new_selected_item_y;
for(i = 0; i < model->reserve_x; ++i) {
new_selected_item_x = (model->selected_item_x + i) % model->reserve_x;
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
break;
}
}
if(i == model->reserve_x) return false;
model->selected_item_x = new_selected_item_x;
model->selected_item_y = new_selected_item_y;
return true;
});
}
static void button_panel_process_left(ButtonPanel* button_panel) {
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
size_t new_selected_item_x = model->selected_item_x;
size_t new_selected_item_y = model->selected_item_y;
size_t i;
if(new_selected_item_x <= 0) return false;
--new_selected_item_x;
for(i = 0; i < model->reserve_y; ++i) {
new_selected_item_y = (model->selected_item_y + i) % model->reserve_y;
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
break;
}
}
if(i == model->reserve_y) return false;
model->selected_item_x = new_selected_item_x;
model->selected_item_y = new_selected_item_y;
return true;
});
}
static void button_panel_process_right(ButtonPanel* button_panel) {
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
uint16_t new_selected_item_x = model->selected_item_x;
uint16_t new_selected_item_y = model->selected_item_y;
size_t i;
if(new_selected_item_x >= (model->reserve_x - 1)) return false;
++new_selected_item_x;
for(i = 0; i < model->reserve_y; ++i) {
new_selected_item_y = (model->selected_item_y + i) % model->reserve_y;
if(*button_panel_get_item(model, new_selected_item_x, new_selected_item_y)) {
break;
}
}
if(i == model->reserve_y) return false;
model->selected_item_x = new_selected_item_x;
model->selected_item_y = new_selected_item_y;
return true;
});
}
void button_panel_process_ok(ButtonPanel* button_panel) {
ButtonItem* button_item = NULL;
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
button_item =
*button_panel_get_item(model, model->selected_item_x, model->selected_item_y);
return true;
});
if(button_item && button_item->callback) {
button_item->callback(button_item->callback_context, button_item->index);
}
}
static bool button_panel_view_input_callback(InputEvent* event, void* context) {
ButtonPanel* button_panel = context;
furi_assert(button_panel);
bool consumed = false;
if(event->type == InputTypeShort) {
switch(event->key) {
case InputKeyUp:
consumed = true;
button_panel_process_up(button_panel);
break;
case InputKeyDown:
consumed = true;
button_panel_process_down(button_panel);
break;
case InputKeyLeft:
consumed = true;
button_panel_process_left(button_panel);
break;
case InputKeyRight:
consumed = true;
button_panel_process_right(button_panel);
break;
case InputKeyOk:
consumed = true;
button_panel_process_ok(button_panel);
break;
default:
break;
}
}
return consumed;
}
void button_panel_add_label(
ButtonPanel* button_panel,
uint16_t x,
uint16_t y,
Font font,
const char* label_str) {
furi_assert(button_panel);
with_view_model(
button_panel->view, (ButtonPanelModel * model) {
LabelElement* label = LabelList_push_raw(model->labels);
label->x = x;
label->y = y;
label->font = font;
label->str = label_str;
return true;
});
}

View File

@@ -0,0 +1,105 @@
/**
* @file button_panel.h
* GUI: ButtonPanel view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Button panel module descriptor */
typedef struct ButtonPanel ButtonPanel;
/** Callback type to call for handling selecting button_panel items */
typedef void (*ButtonItemCallback)(void* context, uint32_t index);
/** Allocate new button_panel module.
*
* @return ButtonPanel instance
*/
ButtonPanel* button_panel_alloc(void);
/** Free button_panel module.
*
* @param button_panel ButtonPanel instance
*/
void button_panel_free(ButtonPanel* button_panel);
/** Free items from button_panel module. Preallocated matrix stays unchanged.
*
* @param button_panel ButtonPanel instance
*/
void button_panel_reset(ButtonPanel* button_panel);
/** Reserve space for adding items.
*
* One does not simply use button_panel_add_item() without this function. It
* should be allocated space for it first.
*
* @param button_panel ButtonPanel instance
* @param reserve_x number of columns in button_panel
* @param reserve_y number of rows in button_panel
*/
void button_panel_reserve(ButtonPanel* button_panel, size_t reserve_x, size_t reserve_y);
/** Add item to button_panel module.
*
* Have to set element in bounds of allocated size by X and by Y.
*
* @param button_panel ButtonPanel instance
* @param index value to pass to callback
* @param matrix_place_x coordinates by x-axis on virtual grid, it
* is only used for naviagation
* @param matrix_place_y coordinates by y-axis on virtual grid, it
* is only used for naviagation
* @param x x-coordinate to draw icon on
* @param y y-coordinate to draw icon on
* @param icon_name name of the icon to draw
* @param icon_name_selected name of the icon to draw when current
* element is selected
* @param callback function to call when specific element is
* selected (pressed Ok on selected item)
* @param callback_context context to pass to callback
*/
void button_panel_add_item(
ButtonPanel* button_panel,
uint32_t index,
uint16_t matrix_place_x,
uint16_t matrix_place_y,
uint16_t x,
uint16_t y,
const Icon* icon_name,
const Icon* icon_name_selected,
ButtonItemCallback callback,
void* callback_context);
/** Get button_panel view.
*
* @param button_panel ButtonPanel instance
*
* @return acquired view
*/
View* button_panel_get_view(ButtonPanel* button_panel);
/** Add label to button_panel module.
*
* @param button_panel ButtonPanel instance
* @param x x-coordinate to place label
* @param y y-coordinate to place label
* @param font font to write label with
* @param label_str string label to write
*/
void button_panel_add_label(
ButtonPanel* button_panel,
uint16_t x,
uint16_t y,
Font font,
const char* label_str);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,781 @@
#include "byte_input.h"
#include <gui/elements.h>
#include <furi.h>
struct ByteInput {
View* view;
};
typedef struct {
const uint8_t value;
const uint8_t x;
const uint8_t y;
} ByteInputKey;
typedef struct {
const char* header;
uint8_t* bytes;
uint8_t bytes_count;
ByteInputCallback input_callback;
ByteChangedCallback changed_callback;
void* callback_context;
bool selected_high_nibble;
uint8_t selected_byte;
int8_t selected_row; // row -1 - input, row 0 & 1 - keyboard
uint8_t selected_column;
uint8_t first_visible_byte;
} ByteInputModel;
static const uint8_t keyboard_origin_x = 7;
static const uint8_t keyboard_origin_y = 31;
static const uint8_t keyboard_row_count = 2;
static const uint8_t enter_symbol = '\r';
static const uint8_t backspace_symbol = '\b';
static const uint8_t max_drawable_bytes = 8;
static const ByteInputKey keyboard_keys_row_1[] = {
{'0', 0, 12},
{'1', 11, 12},
{'2', 22, 12},
{'3', 33, 12},
{'4', 44, 12},
{'5', 55, 12},
{'6', 66, 12},
{'7', 77, 12},
{backspace_symbol, 103, 4},
};
static const ByteInputKey keyboard_keys_row_2[] = {
{'8', 0, 26},
{'9', 11, 26},
{'A', 22, 26},
{'B', 33, 26},
{'C', 44, 26},
{'D', 55, 26},
{'E', 66, 26},
{'F', 77, 26},
{enter_symbol, 95, 17},
};
/**
* @brief Get row size
*
* @param row_index Index of row
* @return uint8_t Row size
*/
static uint8_t byte_input_get_row_size(uint8_t row_index) {
uint8_t row_size = 0;
switch(row_index + 1) {
case 1:
row_size = sizeof(keyboard_keys_row_1) / sizeof(ByteInputKey);
break;
case 2:
row_size = sizeof(keyboard_keys_row_2) / sizeof(ByteInputKey);
break;
}
return row_size;
}
/**
* @brief Get row pointer
*
* @param row_index Index of row
* @return const ByteInputKey* Row pointer
*/
static const ByteInputKey* byte_input_get_row(uint8_t row_index) {
const ByteInputKey* row = NULL;
switch(row_index + 1) {
case 1:
row = keyboard_keys_row_1;
break;
case 2:
row = keyboard_keys_row_2;
break;
}
return row;
}
/**
* @brief Get text from nibble
*
* @param byte byte value
* @param high_nibble Get from high nibble, otherwise low nibble
* @return char nibble text
*/
static char byte_input_get_nibble_text(uint8_t byte, bool high_nibble) {
if(high_nibble) {
byte = byte >> 4;
}
byte = byte & 0x0F;
switch(byte & 0x0F) {
case 0x0:
case 0x1:
case 0x2:
case 0x3:
case 0x4:
case 0x5:
case 0x6:
case 0x7:
case 0x8:
case 0x9:
byte = byte + '0';
break;
case 0xA:
case 0xB:
case 0xC:
case 0xD:
case 0xE:
case 0xF:
byte = byte - 0xA + 'A';
break;
default:
byte = '!';
break;
}
return byte;
}
/**
* @brief Draw input box (common view)
*
* @param canvas
* @param model
*/
static void byte_input_draw_input(Canvas* canvas, ByteInputModel* model) {
const uint8_t text_x = 8;
const uint8_t text_y = 25;
elements_slightly_rounded_frame(canvas, 6, 14, 116, 15);
canvas_draw_icon(canvas, 2, 19, &I_ButtonLeftSmall_3x5);
canvas_draw_icon(canvas, 123, 19, &I_ButtonRightSmall_3x5);
for(uint8_t i = model->first_visible_byte;
i < model->first_visible_byte + MIN(model->bytes_count, max_drawable_bytes);
i++) {
uint8_t byte_position = i - model->first_visible_byte;
if(i == model->selected_byte) {
canvas_draw_frame(canvas, text_x + byte_position * 14, text_y - 9, 15, 11);
if(model->selected_high_nibble) {
canvas_draw_glyph(
canvas,
text_x + 8 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], false));
canvas_draw_box(canvas, text_x + 1 + byte_position * 14, text_y - 8, 7, 9);
canvas_invert_color(canvas);
canvas_draw_line(
canvas,
text_x + 14 + byte_position * 14,
text_y - 6,
text_x + 14 + byte_position * 14,
text_y - 2);
canvas_draw_glyph(
canvas,
text_x + 2 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], true));
canvas_invert_color(canvas);
} else {
canvas_draw_box(canvas, text_x + 7 + byte_position * 14, text_y - 8, 7, 9);
canvas_draw_glyph(
canvas,
text_x + 2 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], true));
canvas_invert_color(canvas);
canvas_draw_line(
canvas,
text_x + byte_position * 14,
text_y - 6,
text_x + byte_position * 14,
text_y - 2);
canvas_draw_glyph(
canvas,
text_x + 8 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], false));
canvas_invert_color(canvas);
}
} else {
canvas_draw_glyph(
canvas,
text_x + 2 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], true));
canvas_draw_glyph(
canvas,
text_x + 8 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], false));
}
}
if(model->bytes_count - model->first_visible_byte > max_drawable_bytes) {
canvas_draw_icon(canvas, 123, 21, &I_ButtonRightSmall_3x5);
}
if(model->first_visible_byte > 0) {
canvas_draw_icon(canvas, 1, 21, &I_ButtonLeftSmall_3x5);
}
}
/**
* @brief Draw input box (selected view)
*
* @param canvas
* @param model
*/
static void byte_input_draw_input_selected(Canvas* canvas, ByteInputModel* model) {
const uint8_t text_x = 7;
const uint8_t text_y = 25;
canvas_draw_box(canvas, 0, 12, 127, 19);
canvas_invert_color(canvas);
elements_slightly_rounded_frame(canvas, 6, 14, 115, 15);
canvas_draw_icon(canvas, 2, 19, &I_ButtonLeftSmall_3x5);
canvas_draw_icon(canvas, 122, 19, &I_ButtonRightSmall_3x5);
for(uint8_t i = model->first_visible_byte;
i < model->first_visible_byte + MIN(model->bytes_count, max_drawable_bytes);
i++) {
uint8_t byte_position = i - model->first_visible_byte;
if(i == model->selected_byte) {
canvas_draw_box(canvas, text_x + 1 + byte_position * 14, text_y - 9, 13, 11);
canvas_invert_color(canvas);
canvas_draw_glyph(
canvas,
text_x + 2 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], true));
canvas_draw_glyph(
canvas,
text_x + 8 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], false));
canvas_invert_color(canvas);
} else {
canvas_draw_glyph(
canvas,
text_x + 2 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], true));
canvas_draw_glyph(
canvas,
text_x + 8 + byte_position * 14,
text_y,
byte_input_get_nibble_text(model->bytes[i], false));
}
}
if(model->bytes_count - model->first_visible_byte > max_drawable_bytes) {
canvas_draw_icon(canvas, 123, 21, &I_ButtonRightSmall_3x5);
}
if(model->first_visible_byte > 0) {
canvas_draw_icon(canvas, 1, 21, &I_ButtonLeftSmall_3x5);
}
canvas_invert_color(canvas);
}
/**
* @brief Set nibble at position
*
* @param data where to set nibble
* @param position byte position
* @param value char value
* @param high_nibble set high nibble
*/
static void byte_input_set_nibble(uint8_t* data, uint8_t position, char value, bool high_nibble) {
switch(value) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
value = value - '0';
break;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
value = value - 'A' + 10;
break;
default:
value = 0;
break;
}
if(high_nibble) {
data[position] &= 0x0F;
data[position] |= value << 4;
} else {
data[position] &= 0xF0;
data[position] |= value;
}
}
/**
* @brief What currently selected
*
* @return true - keyboard selected, false - input selected
*/
static bool byte_input_keyboard_selected(ByteInputModel* model) {
return model->selected_row >= 0;
}
/**
* @brief Do transition from keyboard
*
* @param model
*/
static void byte_input_transition_from_keyboard(ByteInputModel* model) {
model->selected_row += 1;
model->selected_high_nibble = true;
}
/**
* @brief Increase selected byte position
*
* @param model
*/
static void byte_input_inc_selected_byte(ByteInputModel* model) {
if(model->selected_byte < model->bytes_count - 1) {
model->selected_byte += 1;
if(model->bytes_count > max_drawable_bytes) {
if(model->selected_byte - model->first_visible_byte > (max_drawable_bytes - 2)) {
if(model->first_visible_byte < model->bytes_count - max_drawable_bytes) {
model->first_visible_byte++;
}
}
}
}
}
/**
* @brief Decrease selected byte position
*
* @param model
*/
static void byte_input_dec_selected_byte(ByteInputModel* model) {
if(model->selected_byte > 0) {
model->selected_byte -= 1;
if(model->selected_byte - model->first_visible_byte < 1) {
if(model->first_visible_byte > 0) {
model->first_visible_byte--;
}
}
}
}
/**
* @brief Call input callback
*
* @param model
*/
static void byte_input_call_input_callback(ByteInputModel* model) {
if(model->input_callback != NULL) {
model->input_callback(model->callback_context);
}
}
/**
* @brief Call changed callback
*
* @param model
*/
static void byte_input_call_changed_callback(ByteInputModel* model) {
if(model->changed_callback != NULL) {
model->changed_callback(model->callback_context);
}
}
/**
* @brief Clear selected byte
*/
static void byte_input_clear_selected_byte(ByteInputModel* model) {
model->bytes[model->selected_byte] = 0;
model->selected_high_nibble = true;
byte_input_dec_selected_byte(model);
byte_input_call_changed_callback(model);
}
/**
* @brief Handle up button
*
* @param model
*/
static void byte_input_handle_up(ByteInputModel* model) {
if(model->selected_row > -1) {
model->selected_row -= 1;
}
}
/**
* @brief Handle down button
*
* @param model
*/
static void byte_input_handle_down(ByteInputModel* model) {
if(byte_input_keyboard_selected(model)) {
if(model->selected_row < keyboard_row_count - 1) {
model->selected_row += 1;
}
} else {
byte_input_transition_from_keyboard(model);
}
}
/**
* @brief Handle left button
*
* @param model
*/
static void byte_input_handle_left(ByteInputModel* model) {
if(byte_input_keyboard_selected(model)) {
if(model->selected_column > 0) {
model->selected_column -= 1;
} else {
model->selected_column = byte_input_get_row_size(model->selected_row) - 1;
}
} else {
byte_input_dec_selected_byte(model);
}
}
/**
* @brief Handle right button
*
* @param model
*/
static void byte_input_handle_right(ByteInputModel* model) {
if(byte_input_keyboard_selected(model)) {
if(model->selected_column < byte_input_get_row_size(model->selected_row) - 1) {
model->selected_column += 1;
} else {
model->selected_column = 0;
}
} else {
byte_input_inc_selected_byte(model);
}
}
/**
* @brief Handle OK button
*
* @param model
*/
static void byte_input_handle_ok(ByteInputModel* model) {
if(byte_input_keyboard_selected(model)) {
uint8_t value = byte_input_get_row(model->selected_row)[model->selected_column].value;
if(value == enter_symbol) {
byte_input_call_input_callback(model);
} else if(value == backspace_symbol) {
byte_input_clear_selected_byte(model);
} else {
byte_input_set_nibble(
model->bytes, model->selected_byte, value, model->selected_high_nibble);
if(model->selected_high_nibble == true) {
model->selected_high_nibble = false;
} else {
byte_input_inc_selected_byte(model);
model->selected_high_nibble = true;
}
byte_input_call_changed_callback(model);
}
} else {
byte_input_transition_from_keyboard(model);
}
}
/**
* @brief Draw callback
*
* @param canvas
* @param _model
*/
static void byte_input_view_draw_callback(Canvas* canvas, void* _model) {
ByteInputModel* model = _model;
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
canvas_draw_str(canvas, 2, 9, model->header);
canvas_set_font(canvas, FontKeyboard);
if(model->selected_row == -1) {
byte_input_draw_input_selected(canvas, model);
} else {
byte_input_draw_input(canvas, model);
}
for(uint8_t row = 0; row < keyboard_row_count; row++) {
const uint8_t column_count = byte_input_get_row_size(row);
const ByteInputKey* keys = byte_input_get_row(row);
for(size_t column = 0; column < column_count; column++) {
if(keys[column].value == enter_symbol) {
canvas_set_color(canvas, ColorBlack);
if(model->selected_row == row && model->selected_column == column) {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeySaveSelected_24x11);
} else {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeySave_24x11);
}
} else if(keys[column].value == backspace_symbol) {
canvas_set_color(canvas, ColorBlack);
if(model->selected_row == row && model->selected_column == column) {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeyBackspaceSelected_16x9);
} else {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeyBackspace_16x9);
}
} else {
if(model->selected_row == row && model->selected_column == column) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(
canvas,
keyboard_origin_x + keys[column].x - 3,
keyboard_origin_y + keys[column].y - 10,
11,
13);
canvas_set_color(canvas, ColorWhite);
} else if(model->selected_row == -1 && row == 0 && model->selected_column == column) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_frame(
canvas,
keyboard_origin_x + keys[column].x - 3,
keyboard_origin_y + keys[column].y - 10,
11,
13);
} else {
canvas_set_color(canvas, ColorBlack);
}
canvas_draw_glyph(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
keys[column].value);
}
}
}
}
/**
* @brief Input callback
*
* @param event
* @param context
* @return true
* @return false
*/
static bool byte_input_view_input_callback(InputEvent* event, void* context) {
ByteInput* byte_input = context;
furi_assert(byte_input);
bool consumed = false;
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
switch(event->key) {
case InputKeyLeft:
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_handle_left(model);
return true;
});
consumed = true;
break;
case InputKeyRight:
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_handle_right(model);
return true;
});
consumed = true;
break;
case InputKeyUp:
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_handle_up(model);
return true;
});
consumed = true;
break;
case InputKeyDown:
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_handle_down(model);
return true;
});
consumed = true;
break;
case InputKeyOk:
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_handle_ok(model);
return true;
});
consumed = true;
break;
default:
break;
}
}
if((event->type == InputTypeLong || event->type == InputTypeRepeat) &&
event->key == InputKeyBack) {
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_clear_selected_byte(model);
return true;
});
consumed = true;
}
return consumed;
}
/**
* @brief Reset all input-related data in model
*
* @param model ByteInputModel
*/
static void byte_input_reset_model_input_data(ByteInputModel* model) {
model->bytes = NULL;
model->bytes_count = 0;
model->selected_high_nibble = true;
model->selected_byte = 0;
model->selected_row = 0;
model->selected_column = 0;
model->first_visible_byte = 0;
}
/**
* @brief Allocate and initialize byte input. This byte input is used to enter bytes.
*
* @return ByteInput instance pointer
*/
ByteInput* byte_input_alloc() {
ByteInput* byte_input = malloc(sizeof(ByteInput));
byte_input->view = view_alloc();
view_set_context(byte_input->view, byte_input);
view_allocate_model(byte_input->view, ViewModelTypeLocking, sizeof(ByteInputModel));
view_set_draw_callback(byte_input->view, byte_input_view_draw_callback);
view_set_input_callback(byte_input->view, byte_input_view_input_callback);
with_view_model(
byte_input->view, (ByteInputModel * model) {
model->header = "";
model->input_callback = NULL;
model->changed_callback = NULL;
model->callback_context = NULL;
byte_input_reset_model_input_data(model);
return true;
});
return byte_input;
}
/**
* @brief Deinitialize and free byte input
*
* @param byte_input Byte input instance
*/
void byte_input_free(ByteInput* byte_input) {
furi_assert(byte_input);
view_free(byte_input->view);
free(byte_input);
}
/**
* @brief Get byte input view
*
* @param byte_input byte input instance
* @return View instance that can be used for embedding
*/
View* byte_input_get_view(ByteInput* byte_input) {
furi_assert(byte_input);
return byte_input->view;
}
/**
* @brief Deinitialize and free byte input
*
* @param byte_input byte input instance
* @param input_callback input callback fn
* @param changed_callback changed callback fn
* @param callback_context callback context
* @param bytes buffer to use
* @param bytes_count buffer length
*/
void byte_input_set_result_callback(
ByteInput* byte_input,
ByteInputCallback input_callback,
ByteChangedCallback changed_callback,
void* callback_context,
uint8_t* bytes,
uint8_t bytes_count) {
with_view_model(
byte_input->view, (ByteInputModel * model) {
byte_input_reset_model_input_data(model);
model->input_callback = input_callback;
model->changed_callback = changed_callback;
model->callback_context = callback_context;
model->bytes = bytes;
model->bytes_count = bytes_count;
return true;
});
}
/**
* @brief Set byte input header text
*
* @param byte_input byte input instance
* @param text text to be shown
*/
void byte_input_set_header_text(ByteInput* byte_input, const char* text) {
with_view_model(
byte_input->view, (ByteInputModel * model) {
model->header = text;
return true;
});
}

View File

@@ -0,0 +1,69 @@
/**
* @file byte_input.h
* GUI: ByteInput keyboard view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Byte input anonymous structure */
typedef struct ByteInput ByteInput;
/** callback that is executed on save button press */
typedef void (*ByteInputCallback)(void* context);
/** callback that is executed when byte buffer is changed */
typedef void (*ByteChangedCallback)(void* context);
/** Allocate and initialize byte input. This byte input is used to enter bytes.
*
* @return ByteInput instance pointer
*/
ByteInput* byte_input_alloc();
/** Deinitialize and free byte input
*
* @param byte_input Byte input instance
*/
void byte_input_free(ByteInput* byte_input);
/** Get byte input view
*
* @param byte_input byte input instance
*
* @return View instance that can be used for embedding
*/
View* byte_input_get_view(ByteInput* byte_input);
/** Set byte input result callback
*
* @param byte_input byte input instance
* @param input_callback input callback fn
* @param changed_callback changed callback fn
* @param callback_context callback context
* @param bytes buffer to use
* @param bytes_count buffer length
*/
void byte_input_set_result_callback(
ByteInput* byte_input,
ByteInputCallback input_callback,
ByteChangedCallback changed_callback,
void* callback_context,
uint8_t* bytes,
uint8_t bytes_count);
/** Set byte input header text
*
* @param byte_input byte input instance
* @param text text to be shown
*/
void byte_input_set_header_text(ByteInput* byte_input, const char* text);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,303 @@
#include "dialog_ex.h"
#include <gui/elements.h>
#include <furi.h>
struct DialogEx {
View* view;
void* context;
DialogExResultCallback callback;
bool enable_extended_events;
};
typedef struct {
const char* text;
uint8_t x;
uint8_t y;
Align horizontal;
Align vertical;
} TextElement;
typedef struct {
int8_t x;
int8_t y;
const Icon* icon;
} IconElement;
typedef struct {
TextElement header;
TextElement text;
IconElement icon;
const char* left_text;
const char* center_text;
const char* right_text;
} DialogExModel;
static void dialog_ex_view_draw_callback(Canvas* canvas, void* _model) {
DialogExModel* model = _model;
// Prepare canvas
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
if(model->icon.icon != NULL) {
canvas_draw_icon(canvas, model->icon.x, model->icon.y, model->icon.icon);
}
// Draw header
canvas_set_font(canvas, FontPrimary);
if(model->header.text != NULL) {
elements_multiline_text_aligned(
canvas,
model->header.x,
model->header.y,
model->header.horizontal,
model->header.vertical,
model->header.text);
}
// Draw text
canvas_set_font(canvas, FontSecondary);
if(model->text.text != NULL) {
elements_multiline_text_aligned(
canvas,
model->text.x,
model->text.y,
model->text.horizontal,
model->text.vertical,
model->text.text);
}
// Draw buttons
if(model->left_text != NULL) {
elements_button_left(canvas, model->left_text);
}
if(model->center_text != NULL) {
elements_button_center(canvas, model->center_text);
}
if(model->right_text != NULL) {
elements_button_right(canvas, model->right_text);
}
}
static bool dialog_ex_view_input_callback(InputEvent* event, void* context) {
DialogEx* dialog_ex = context;
bool consumed = false;
const char* left_text = NULL;
const char* center_text = NULL;
const char* right_text = NULL;
with_view_model(
dialog_ex->view, (DialogExModel * model) {
left_text = model->left_text;
center_text = model->center_text;
right_text = model->right_text;
return true;
});
if(dialog_ex->callback) {
if(event->type == InputTypeShort) {
if(event->key == InputKeyLeft && left_text != NULL) {
dialog_ex->callback(DialogExResultLeft, dialog_ex->context);
consumed = true;
} else if(event->key == InputKeyOk && center_text != NULL) {
dialog_ex->callback(DialogExResultCenter, dialog_ex->context);
consumed = true;
} else if(event->key == InputKeyRight && right_text != NULL) {
dialog_ex->callback(DialogExResultRight, dialog_ex->context);
consumed = true;
}
}
if(event->type == InputTypePress && dialog_ex->enable_extended_events) {
if(event->key == InputKeyLeft && left_text != NULL) {
dialog_ex->callback(DialogExPressLeft, dialog_ex->context);
consumed = true;
} else if(event->key == InputKeyOk && center_text != NULL) {
dialog_ex->callback(DialogExPressCenter, dialog_ex->context);
consumed = true;
} else if(event->key == InputKeyRight && right_text != NULL) {
dialog_ex->callback(DialogExPressRight, dialog_ex->context);
consumed = true;
}
}
if(event->type == InputTypeRelease && dialog_ex->enable_extended_events) {
if(event->key == InputKeyLeft && left_text != NULL) {
dialog_ex->callback(DialogExReleaseLeft, dialog_ex->context);
consumed = true;
} else if(event->key == InputKeyOk && center_text != NULL) {
dialog_ex->callback(DialogExReleaseCenter, dialog_ex->context);
consumed = true;
} else if(event->key == InputKeyRight && right_text != NULL) {
dialog_ex->callback(DialogExReleaseRight, dialog_ex->context);
consumed = true;
}
}
}
return consumed;
}
DialogEx* dialog_ex_alloc() {
DialogEx* dialog_ex = malloc(sizeof(DialogEx));
dialog_ex->view = view_alloc();
view_set_context(dialog_ex->view, dialog_ex);
view_allocate_model(dialog_ex->view, ViewModelTypeLockFree, sizeof(DialogExModel));
view_set_draw_callback(dialog_ex->view, dialog_ex_view_draw_callback);
view_set_input_callback(dialog_ex->view, dialog_ex_view_input_callback);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->header.text = NULL;
model->header.x = 0;
model->header.y = 0;
model->header.horizontal = AlignLeft;
model->header.vertical = AlignBottom;
model->text.text = NULL;
model->text.x = 0;
model->text.y = 0;
model->text.horizontal = AlignLeft;
model->text.vertical = AlignBottom;
model->icon.x = 0;
model->icon.y = 0;
model->icon.icon = NULL;
model->left_text = NULL;
model->center_text = NULL;
model->right_text = NULL;
return true;
});
dialog_ex->enable_extended_events = false;
return dialog_ex;
}
void dialog_ex_free(DialogEx* dialog_ex) {
furi_assert(dialog_ex);
view_free(dialog_ex->view);
free(dialog_ex);
}
View* dialog_ex_get_view(DialogEx* dialog_ex) {
furi_assert(dialog_ex);
return dialog_ex->view;
}
void dialog_ex_set_result_callback(DialogEx* dialog_ex, DialogExResultCallback callback) {
furi_assert(dialog_ex);
dialog_ex->callback = callback;
}
void dialog_ex_set_context(DialogEx* dialog_ex, void* context) {
furi_assert(dialog_ex);
dialog_ex->context = context;
}
void dialog_ex_set_header(
DialogEx* dialog_ex,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical) {
furi_assert(dialog_ex);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->header.text = text;
model->header.x = x;
model->header.y = y;
model->header.horizontal = horizontal;
model->header.vertical = vertical;
return true;
});
}
void dialog_ex_set_text(
DialogEx* dialog_ex,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical) {
furi_assert(dialog_ex);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->text.text = text;
model->text.x = x;
model->text.y = y;
model->text.horizontal = horizontal;
model->text.vertical = vertical;
return true;
});
}
void dialog_ex_set_icon(DialogEx* dialog_ex, uint8_t x, uint8_t y, const Icon* icon) {
furi_assert(dialog_ex);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->icon.x = x;
model->icon.y = y;
model->icon.icon = icon;
return true;
});
}
void dialog_ex_set_left_button_text(DialogEx* dialog_ex, const char* text) {
furi_assert(dialog_ex);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->left_text = text;
return true;
});
}
void dialog_ex_set_center_button_text(DialogEx* dialog_ex, const char* text) {
furi_assert(dialog_ex);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->center_text = text;
return true;
});
}
void dialog_ex_set_right_button_text(DialogEx* dialog_ex, const char* text) {
furi_assert(dialog_ex);
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->right_text = text;
return true;
});
}
void dialog_ex_reset(DialogEx* dialog_ex) {
furi_assert(dialog_ex);
TextElement clean_text_el = {
.text = NULL, .x = 0, .y = 0, .horizontal = AlignLeft, .vertical = AlignLeft};
IconElement clean_icon_el = {.icon = NULL, .x = 0, .y = 0};
with_view_model(
dialog_ex->view, (DialogExModel * model) {
model->header = clean_text_el;
model->text = clean_text_el;
model->icon = clean_icon_el;
model->left_text = NULL;
model->center_text = NULL;
model->right_text = NULL;
return true;
});
dialog_ex->context = NULL;
dialog_ex->callback = NULL;
}
void dialog_ex_enable_extended_events(DialogEx* dialog_ex) {
furi_assert(dialog_ex);
dialog_ex->enable_extended_events = true;
}
void dialog_ex_disable_extended_events(DialogEx* dialog_ex) {
furi_assert(dialog_ex);
dialog_ex->enable_extended_events = false;
}

View File

@@ -0,0 +1,168 @@
/**
* @file dialog_ex.h
* GUI: DialogEx view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Dialog anonymous structure */
typedef struct DialogEx DialogEx;
/** DialogEx result */
typedef enum {
DialogExResultLeft,
DialogExResultCenter,
DialogExResultRight,
DialogExPressLeft,
DialogExPressCenter,
DialogExPressRight,
DialogExReleaseLeft,
DialogExReleaseCenter,
DialogExReleaseRight,
} DialogExResult;
/** DialogEx result callback type
* @warning comes from GUI thread
*/
typedef void (*DialogExResultCallback)(DialogExResult result, void* context);
/** Allocate and initialize dialog
*
* This dialog used to ask simple questions
*
* @return DialogEx instance
*/
DialogEx* dialog_ex_alloc();
/** Deinitialize and free dialog
*
* @param dialog_ex DialogEx instance
*/
void dialog_ex_free(DialogEx* dialog_ex);
/** Get dialog view
*
* @param dialog_ex DialogEx instance
*
* @return View instance that can be used for embedding
*/
View* dialog_ex_get_view(DialogEx* dialog_ex);
/** Set dialog result callback
*
* @param dialog_ex DialogEx instance
* @param callback result callback function
*/
void dialog_ex_set_result_callback(DialogEx* dialog_ex, DialogExResultCallback callback);
/** Set dialog context
*
* @param dialog_ex DialogEx instance
* @param context context pointer, will be passed to result callback
*/
void dialog_ex_set_context(DialogEx* dialog_ex, void* context);
/** Set dialog header text
*
* If text is null, dialog header will not be rendered
*
* @param dialog_ex DialogEx instance
* @param text text to be shown, can be multiline
* @param x x position
* @param y y position
* @param horizontal horizontal text aligment
* @param vertical vertical text aligment
*/
void dialog_ex_set_header(
DialogEx* dialog_ex,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical);
/** Set dialog text
*
* If text is null, dialog text will not be rendered
*
* @param dialog_ex DialogEx instance
* @param text text to be shown, can be multiline
* @param x x position
* @param y y position
* @param horizontal horizontal text aligment
* @param vertical vertical text aligment
*/
void dialog_ex_set_text(
DialogEx* dialog_ex,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical);
/** Set dialog icon
*
* If x or y is negative, dialog icon will not be rendered
*
* @param dialog_ex DialogEx instance
* @param x x position
* @param y y position
* @param icon The icon
* @param name icon to be shown
*/
void dialog_ex_set_icon(DialogEx* dialog_ex, uint8_t x, uint8_t y, const Icon* icon);
/** Set left button text
*
* If text is null, left button will not be rendered and processed
*
* @param dialog_ex DialogEx instance
* @param text text to be shown
*/
void dialog_ex_set_left_button_text(DialogEx* dialog_ex, const char* text);
/** Set center button text
*
* If text is null, center button will not be rendered and processed
*
* @param dialog_ex DialogEx instance
* @param text text to be shown
*/
void dialog_ex_set_center_button_text(DialogEx* dialog_ex, const char* text);
/** Set right button text
*
* If text is null, right button will not be rendered and processed
*
* @param dialog_ex DialogEx instance
* @param text text to be shown
*/
void dialog_ex_set_right_button_text(DialogEx* dialog_ex, const char* text);
/** Clean dialog
*
* @param dialog_ex DialogEx instance
*/
void dialog_ex_reset(DialogEx* dialog_ex);
/** Enable press/release events
*
* @param dialog_ex DialogEx instance
*/
void dialog_ex_enable_extended_events(DialogEx* dialog_ex);
/** Disable press/release events
*
* @param dialog_ex DialogEx instance
*/
void dialog_ex_disable_extended_events(DialogEx* dialog_ex);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,37 @@
#include "empty_screen.h"
#include <furi.h>
struct EmptyScreen {
View* view;
};
static void empty_screen_view_draw_callback(Canvas* canvas, void* _model) {
UNUSED(_model);
canvas_clear(canvas);
}
static bool empty_screen_view_input_callback(InputEvent* event, void* context) {
UNUSED(event);
UNUSED(context);
return false;
}
EmptyScreen* empty_screen_alloc() {
EmptyScreen* empty_screen = malloc(sizeof(EmptyScreen));
empty_screen->view = view_alloc();
view_set_context(empty_screen->view, empty_screen);
view_set_draw_callback(empty_screen->view, empty_screen_view_draw_callback);
view_set_input_callback(empty_screen->view, empty_screen_view_input_callback);
return empty_screen;
}
void empty_screen_free(EmptyScreen* empty_screen) {
furi_assert(empty_screen);
view_free(empty_screen->view);
free(empty_screen);
}
View* empty_screen_get_view(EmptyScreen* empty_screen) {
furi_assert(empty_screen);
return empty_screen->view;
}

View File

@@ -0,0 +1,41 @@
/**
* @file empty_screen.h
* GUI: EmptyScreen view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Empty screen anonymous structure */
typedef struct EmptyScreen EmptyScreen;
/** Allocate and initialize empty screen
*
* This empty screen used to ask simple questions like Yes/
*
* @return EmptyScreen instance
*/
EmptyScreen* empty_screen_alloc();
/** Deinitialize and free empty screen
*
* @param empty_screen Empty screen instance
*/
void empty_screen_free(EmptyScreen* empty_screen);
/** Get empty screen view
*
* @param empty_screen Empty screen instance
*
* @return View instance that can be used for embedding
*/
View* empty_screen_get_view(EmptyScreen* empty_screen);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,595 @@
#include "file_browser.h"
#include "assets_icons.h"
#include "file_browser_worker.h"
#include <core/check.h>
#include <core/common_defines.h>
#include <core/log.h>
#include "furi_hal_resources.h"
#include "m-string.h"
#include <m-array.h>
#include <gui/elements.h>
#include <furi.h>
#include "toolbox/path.h"
#define LIST_ITEMS 5u
#define MAX_LEN_PX 110
#define FRAME_HEIGHT 12
#define Y_OFFSET 3
#define ITEM_LIST_LEN_MAX 50
#define CUSTOM_ICON_MAX_SIZE 32
typedef enum {
BrowserItemTypeLoading,
BrowserItemTypeBack,
BrowserItemTypeFolder,
BrowserItemTypeFile,
} BrowserItemType;
typedef struct {
string_t path;
BrowserItemType type;
uint8_t* custom_icon_data;
string_t display_name;
} BrowserItem_t;
static void BrowserItem_t_init(BrowserItem_t* obj) {
obj->type = BrowserItemTypeLoading;
string_init(obj->path);
string_init(obj->display_name);
obj->custom_icon_data = NULL;
}
static void BrowserItem_t_init_set(BrowserItem_t* obj, const BrowserItem_t* src) {
obj->type = src->type;
string_init_set(obj->path, src->path);
string_init_set(obj->display_name, src->display_name);
if(src->custom_icon_data) {
obj->custom_icon_data = malloc(CUSTOM_ICON_MAX_SIZE);
memcpy(obj->custom_icon_data, src->custom_icon_data, CUSTOM_ICON_MAX_SIZE);
} else {
obj->custom_icon_data = NULL;
}
}
static void BrowserItem_t_set(BrowserItem_t* obj, const BrowserItem_t* src) {
obj->type = src->type;
string_set(obj->path, src->path);
string_set(obj->display_name, src->display_name);
if(src->custom_icon_data) {
obj->custom_icon_data = malloc(CUSTOM_ICON_MAX_SIZE);
memcpy(obj->custom_icon_data, src->custom_icon_data, CUSTOM_ICON_MAX_SIZE);
} else {
obj->custom_icon_data = NULL;
}
}
static void BrowserItem_t_clear(BrowserItem_t* obj) {
string_clear(obj->path);
string_clear(obj->display_name);
if(obj->custom_icon_data) {
free(obj->custom_icon_data);
}
}
ARRAY_DEF(
items_array,
BrowserItem_t,
(INIT(API_2(BrowserItem_t_init)),
SET(API_6(BrowserItem_t_set)),
INIT_SET(API_6(BrowserItem_t_init_set)),
CLEAR(API_2(BrowserItem_t_clear))))
struct FileBrowser {
View* view;
BrowserWorker* worker;
const char* ext_filter;
bool skip_assets;
bool hide_ext;
FileBrowserCallback callback;
void* context;
FileBrowserLoadItemCallback item_callback;
void* item_context;
string_ptr result_path;
};
typedef struct {
items_array_t items;
bool is_root;
bool folder_loading;
bool list_loading;
uint32_t item_cnt;
int32_t item_idx;
int32_t array_offset;
int32_t list_offset;
const Icon* file_icon;
bool hide_ext;
} FileBrowserModel;
static const Icon* BrowserItemIcons[] = {
[BrowserItemTypeLoading] = &I_loading_10px,
[BrowserItemTypeBack] = &I_back_10px,
[BrowserItemTypeFolder] = &I_dir_10px,
[BrowserItemTypeFile] = &I_unknown_10px,
};
static void file_browser_view_draw_callback(Canvas* canvas, void* _model);
static bool file_browser_view_input_callback(InputEvent* event, void* context);
static void
browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root);
static void browser_list_load_cb(void* context, uint32_t list_load_offset);
static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last);
static void browser_long_load_cb(void* context);
FileBrowser* file_browser_alloc(string_ptr result_path) {
furi_assert(result_path);
FileBrowser* browser = malloc(sizeof(FileBrowser));
browser->view = view_alloc();
view_allocate_model(browser->view, ViewModelTypeLocking, sizeof(FileBrowserModel));
view_set_context(browser->view, browser);
view_set_draw_callback(browser->view, file_browser_view_draw_callback);
view_set_input_callback(browser->view, file_browser_view_input_callback);
browser->result_path = result_path;
with_view_model(
browser->view, (FileBrowserModel * model) {
items_array_init(model->items);
return false;
});
return browser;
}
void file_browser_free(FileBrowser* browser) {
furi_assert(browser);
with_view_model(
browser->view, (FileBrowserModel * model) {
items_array_clear(model->items);
return false;
});
view_free(browser->view);
free(browser);
}
View* file_browser_get_view(FileBrowser* browser) {
furi_assert(browser);
return browser->view;
}
void file_browser_configure(
FileBrowser* browser,
const char* extension,
bool skip_assets,
const Icon* file_icon,
bool hide_ext) {
furi_assert(browser);
browser->ext_filter = extension;
browser->skip_assets = skip_assets;
browser->hide_ext = hide_ext;
with_view_model(
browser->view, (FileBrowserModel * model) {
model->file_icon = file_icon;
model->hide_ext = hide_ext;
return false;
});
}
void file_browser_start(FileBrowser* browser, string_t path) {
furi_assert(browser);
browser->worker = file_browser_worker_alloc(path, browser->ext_filter, browser->skip_assets);
file_browser_worker_set_callback_context(browser->worker, browser);
file_browser_worker_set_folder_callback(browser->worker, browser_folder_open_cb);
file_browser_worker_set_list_callback(browser->worker, browser_list_load_cb);
file_browser_worker_set_item_callback(browser->worker, browser_list_item_cb);
file_browser_worker_set_long_load_callback(browser->worker, browser_long_load_cb);
}
void file_browser_stop(FileBrowser* browser) {
furi_assert(browser);
file_browser_worker_free(browser->worker);
with_view_model(
browser->view, (FileBrowserModel * model) {
items_array_reset(model->items);
model->item_cnt = 0;
model->item_idx = 0;
model->array_offset = 0;
model->list_offset = 0;
return false;
});
}
void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context) {
browser->context = context;
browser->callback = callback;
}
void file_browser_set_item_callback(
FileBrowser* browser,
FileBrowserLoadItemCallback callback,
void* context) {
browser->item_context = context;
browser->item_callback = callback;
}
static bool browser_is_item_in_array(FileBrowserModel* model, uint32_t idx) {
size_t array_size = items_array_size(model->items);
if((idx >= (uint32_t)model->array_offset + array_size) ||
(idx < (uint32_t)model->array_offset)) {
return false;
}
return true;
}
static bool browser_is_list_load_required(FileBrowserModel* model) {
size_t array_size = items_array_size(model->items);
uint32_t item_cnt = (model->is_root) ? model->item_cnt : model->item_cnt - 1;
if((model->list_loading) || (array_size >= item_cnt)) {
return false;
}
if((model->array_offset > 0) &&
(model->item_idx < (model->array_offset + ITEM_LIST_LEN_MAX / 4))) {
return true;
}
if(((model->array_offset + array_size) < item_cnt) &&
(model->item_idx > (int32_t)(model->array_offset + array_size - ITEM_LIST_LEN_MAX / 4))) {
return true;
}
return false;
}
static void browser_update_offset(FileBrowser* browser) {
furi_assert(browser);
with_view_model(
browser->view, (FileBrowserModel * model) {
uint16_t bounds = model->item_cnt > (LIST_ITEMS - 1) ? 2 : model->item_cnt;
if((model->item_cnt > (LIST_ITEMS - 1)) &&
(model->item_idx >= ((int32_t)model->item_cnt - 1))) {
model->list_offset = model->item_idx - (LIST_ITEMS - 1);
} else if(model->list_offset < model->item_idx - bounds) {
model->list_offset = CLAMP(
model->item_idx - (int32_t)(LIST_ITEMS - 2),
(int32_t)model->item_cnt - bounds,
0);
} else if(model->list_offset > model->item_idx - bounds) {
model->list_offset =
CLAMP(model->item_idx - 1, (int32_t)model->item_cnt - bounds, 0);
}
return false;
});
}
static void
browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root) {
furi_assert(context);
FileBrowser* browser = (FileBrowser*)context;
int32_t load_offset = 0;
with_view_model(
browser->view, (FileBrowserModel * model) {
items_array_reset(model->items);
if(is_root) {
model->item_cnt = item_cnt;
model->item_idx = (file_idx > 0) ? file_idx : 0;
load_offset =
CLAMP(model->item_idx - ITEM_LIST_LEN_MAX / 2, (int32_t)model->item_cnt, 0);
} else {
model->item_cnt = item_cnt + 1;
model->item_idx = file_idx + 1;
load_offset = CLAMP(
model->item_idx - ITEM_LIST_LEN_MAX / 2 - 1, (int32_t)model->item_cnt - 1, 0);
}
model->array_offset = 0;
model->list_offset = 0;
model->is_root = is_root;
model->list_loading = true;
model->folder_loading = false;
return true;
});
browser_update_offset(browser);
file_browser_worker_load(browser->worker, load_offset, ITEM_LIST_LEN_MAX);
}
static void browser_list_load_cb(void* context, uint32_t list_load_offset) {
furi_assert(context);
FileBrowser* browser = (FileBrowser*)context;
BrowserItem_t back_item;
BrowserItem_t_init(&back_item);
back_item.type = BrowserItemTypeBack;
with_view_model(
browser->view, (FileBrowserModel * model) {
items_array_reset(model->items);
model->array_offset = list_load_offset;
if(!model->is_root) {
if(list_load_offset == 0) {
items_array_push_back(model->items, back_item);
} else {
model->array_offset += 1;
}
}
return false;
});
BrowserItem_t_clear(&back_item);
}
static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last) {
furi_assert(context);
FileBrowser* browser = (FileBrowser*)context;
BrowserItem_t item;
item.custom_icon_data = NULL;
if(!is_last) {
string_init_set(item.path, item_path);
string_init(item.display_name);
if(is_folder) {
item.type = BrowserItemTypeFolder;
} else {
item.type = BrowserItemTypeFile;
if(browser->item_callback) {
item.custom_icon_data = malloc(CUSTOM_ICON_MAX_SIZE);
if(!browser->item_callback(
item_path,
browser->item_context,
&item.custom_icon_data,
item.display_name)) {
free(item.custom_icon_data);
item.custom_icon_data = NULL;
}
}
}
if(string_empty_p(item.display_name)) {
path_extract_filename(
item_path,
item.display_name,
(browser->hide_ext) && (item.type == BrowserItemTypeFile));
}
with_view_model(
browser->view, (FileBrowserModel * model) {
items_array_push_back(model->items, item);
// TODO: calculate if element is visible
return true;
});
string_clear(item.display_name);
string_clear(item.path);
} else {
with_view_model(
browser->view, (FileBrowserModel * model) {
model->list_loading = false;
return true;
});
}
}
static void browser_long_load_cb(void* context) {
furi_assert(context);
FileBrowser* browser = (FileBrowser*)context;
with_view_model(
browser->view, (FileBrowserModel * model) {
model->folder_loading = true;
return true;
});
}
static void browser_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(
canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT, (scrollbar ? 122 : 127), FRAME_HEIGHT);
canvas_set_color(canvas, ColorWhite);
canvas_draw_dot(canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT);
canvas_draw_dot(canvas, 1, Y_OFFSET + idx * FRAME_HEIGHT);
canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + 1);
canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1));
canvas_draw_dot(canvas, scrollbar ? 121 : 126, Y_OFFSET + idx * FRAME_HEIGHT);
canvas_draw_dot(
canvas, scrollbar ? 121 : 126, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1));
}
static void browser_draw_loading(Canvas* canvas, FileBrowserModel* model) {
UNUSED(model);
uint8_t x = 128 / 2 - 24 / 2;
uint8_t y = 64 / 2 - 24 / 2;
canvas_draw_icon(canvas, x, y, &A_Loading_24);
}
static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) {
uint32_t array_size = items_array_size(model->items);
bool show_scrollbar = model->item_cnt > LIST_ITEMS;
string_t filename;
string_init(filename);
for(uint32_t i = 0; i < MIN(model->item_cnt, LIST_ITEMS); i++) {
int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u);
BrowserItemType item_type = BrowserItemTypeLoading;
uint8_t* custom_icon_data = NULL;
if(browser_is_item_in_array(model, idx)) {
BrowserItem_t* item = items_array_get(
model->items, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0));
item_type = item->type;
string_set(filename, item->display_name);
if(item_type == BrowserItemTypeFile) {
custom_icon_data = item->custom_icon_data;
}
} else {
string_set_str(filename, "---");
}
if(item_type == BrowserItemTypeBack) {
string_set_str(filename, ". .");
}
elements_string_fit_width(
canvas, filename, (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX));
if(model->item_idx == idx) {
browser_draw_frame(canvas, i, show_scrollbar);
} else {
canvas_set_color(canvas, ColorBlack);
}
if(custom_icon_data) {
// Currently only 10*10 icons are supported
canvas_draw_bitmap(
canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, 10, 10, custom_icon_data);
} else if((item_type == BrowserItemTypeFile) && (model->file_icon)) {
canvas_draw_icon(canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, model->file_icon);
} else if(BrowserItemIcons[item_type] != NULL) {
canvas_draw_icon(
canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]);
}
canvas_draw_str(canvas, 15, Y_OFFSET + 9 + i * FRAME_HEIGHT, string_get_cstr(filename));
}
if(show_scrollbar) {
elements_scrollbar_pos(
canvas,
126,
Y_OFFSET,
canvas_height(canvas) - Y_OFFSET,
model->item_idx,
model->item_cnt);
}
string_clear(filename);
}
static void file_browser_view_draw_callback(Canvas* canvas, void* _model) {
FileBrowserModel* model = _model;
if(model->folder_loading) {
browser_draw_loading(canvas, model);
} else {
browser_draw_list(canvas, model);
}
}
static bool file_browser_view_input_callback(InputEvent* event, void* context) {
FileBrowser* browser = context;
furi_assert(browser);
bool consumed = false;
bool is_loading = false;
with_view_model(
browser->view, (FileBrowserModel * model) {
is_loading = model->folder_loading;
return false;
});
if(is_loading) {
return false;
} else if(event->key == InputKeyUp || event->key == InputKeyDown) {
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
with_view_model(
browser->view, (FileBrowserModel * model) {
if(event->key == InputKeyUp) {
model->item_idx =
((model->item_idx - 1) + model->item_cnt) % model->item_cnt;
if(browser_is_list_load_required(model)) {
model->list_loading = true;
int32_t load_offset = CLAMP(
model->item_idx - ITEM_LIST_LEN_MAX / 4 * 3,
(int32_t)model->item_cnt - ITEM_LIST_LEN_MAX,
0);
file_browser_worker_load(
browser->worker, load_offset, ITEM_LIST_LEN_MAX);
}
} else if(event->key == InputKeyDown) {
model->item_idx = (model->item_idx + 1) % model->item_cnt;
if(browser_is_list_load_required(model)) {
model->list_loading = true;
int32_t load_offset = CLAMP(
model->item_idx - ITEM_LIST_LEN_MAX / 4 * 1,
(int32_t)model->item_cnt - ITEM_LIST_LEN_MAX,
0);
file_browser_worker_load(
browser->worker, load_offset, ITEM_LIST_LEN_MAX);
}
}
return true;
});
browser_update_offset(browser);
consumed = true;
}
} else if(event->key == InputKeyOk) {
if(event->type == InputTypeShort) {
BrowserItem_t* selected_item = NULL;
int32_t select_index = 0;
with_view_model(
browser->view, (FileBrowserModel * model) {
if(browser_is_item_in_array(model, model->item_idx)) {
selected_item =
items_array_get(model->items, model->item_idx - model->array_offset);
select_index = model->item_idx;
if((!model->is_root) && (select_index > 0)) {
select_index -= 1;
}
}
return false;
});
if(selected_item) {
if(selected_item->type == BrowserItemTypeBack) {
file_browser_worker_folder_exit(browser->worker);
} else if(selected_item->type == BrowserItemTypeFolder) {
file_browser_worker_folder_enter(
browser->worker, selected_item->path, select_index);
} else if(selected_item->type == BrowserItemTypeFile) {
string_set(browser->result_path, selected_item->path);
if(browser->callback) {
browser->callback(browser->context);
}
}
}
consumed = true;
}
} else if(event->key == InputKeyLeft) {
if(event->type == InputTypeShort) {
bool is_root = false;
with_view_model(
browser->view, (FileBrowserModel * model) {
is_root = model->is_root;
return false;
});
if(!is_root) {
file_browser_worker_folder_exit(browser->worker);
}
consumed = true;
}
}
return consumed;
}

View File

@@ -0,0 +1,47 @@
/**
* @file file_browser.h
* GUI: FileBrowser view module API
*/
#pragma once
#include "m-string.h"
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct FileBrowser FileBrowser;
typedef void (*FileBrowserCallback)(void* context);
typedef bool (
*FileBrowserLoadItemCallback)(string_t path, void* context, uint8_t** icon, string_t item_name);
FileBrowser* file_browser_alloc(string_ptr result_path);
void file_browser_free(FileBrowser* browser);
View* file_browser_get_view(FileBrowser* browser);
void file_browser_configure(
FileBrowser* browser,
const char* extension,
bool skip_assets,
const Icon* file_icon,
bool hide_ext);
void file_browser_start(FileBrowser* browser, string_t path);
void file_browser_stop(FileBrowser* browser);
void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context);
void file_browser_set_item_callback(
FileBrowser* browser,
FileBrowserLoadItemCallback callback,
void* context);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,458 @@
#include "file_browser_worker.h"
#include <core/check.h>
#include <core/common_defines.h>
#include "m-string.h"
#include "storage/filesystem_api_defines.h"
#include <m-array.h>
#include <stdbool.h>
#include <storage/storage.h>
#include <furi.h>
#include <stddef.h>
#include "toolbox/path.h"
#define TAG "BrowserWorker"
#define ASSETS_DIR "assets"
#define BROWSER_ROOT STORAGE_ANY_PATH_PREFIX
#define FILE_NAME_LEN_MAX 256
#define LONG_LOAD_THRESHOLD 100
typedef enum {
WorkerEvtStop = (1 << 0),
WorkerEvtLoad = (1 << 1),
WorkerEvtFolderEnter = (1 << 2),
WorkerEvtFolderExit = (1 << 3),
WorkerEvtFolderRefresh = (1 << 4),
WorkerEvtConfigChange = (1 << 5),
} WorkerEvtFlags;
#define WORKER_FLAGS_ALL \
(WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit | \
WorkerEvtFolderRefresh | WorkerEvtConfigChange)
ARRAY_DEF(idx_last_array, int32_t)
struct BrowserWorker {
FuriThread* thread;
string_t filter_extension;
string_t path_next;
int32_t item_sel_idx;
uint32_t load_offset;
uint32_t load_count;
bool skip_assets;
idx_last_array_t idx_last;
void* cb_ctx;
BrowserWorkerFolderOpenCallback folder_cb;
BrowserWorkerListLoadCallback list_load_cb;
BrowserWorkerListItemCallback list_item_cb;
BrowserWorkerLongLoadCallback long_load_cb;
};
static bool browser_path_is_file(string_t path) {
bool state = false;
FileInfo file_info;
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) {
if((file_info.flags & FSF_DIRECTORY) == 0) {
state = true;
}
}
furi_record_close(RECORD_STORAGE);
return state;
}
static bool browser_path_trim(string_t path) {
bool is_root = false;
size_t filename_start = string_search_rchar(path, '/');
string_left(path, filename_start);
if((string_empty_p(path)) || (filename_start == STRING_FAILURE)) {
string_set_str(path, BROWSER_ROOT);
is_root = true;
}
return is_root;
}
static bool browser_filter_by_name(BrowserWorker* browser, string_t name, bool is_folder) {
if(is_folder) {
// Skip assets folders (if enabled)
if(browser->skip_assets) {
return ((string_cmp_str(name, ASSETS_DIR) == 0) ? (false) : (true));
} else {
return true;
}
} else {
// Filter files by extension
if((string_empty_p(browser->filter_extension)) ||
(string_cmp_str(browser->filter_extension, "*") == 0)) {
return true;
}
if(string_end_with_string_p(name, browser->filter_extension)) {
return true;
}
}
return false;
}
static bool browser_folder_check_and_switch(string_t path) {
FileInfo file_info;
Storage* storage = furi_record_open(RECORD_STORAGE);
bool is_root = false;
if(string_search_rchar(path, '/') == 0) {
is_root = true;
}
while(1) {
// Check if folder is existing and navigate back if not
if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) {
if(file_info.flags & FSF_DIRECTORY) {
break;
}
}
if(is_root) {
break;
}
is_root = browser_path_trim(path);
}
furi_record_close(RECORD_STORAGE);
return is_root;
}
static bool browser_folder_init(
BrowserWorker* browser,
string_t path,
string_t filename,
uint32_t* item_cnt,
int32_t* file_idx) {
bool state = false;
FileInfo file_info;
uint32_t total_files_cnt = 0;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* directory = storage_file_alloc(storage);
char name_temp[FILE_NAME_LEN_MAX];
string_t name_str;
string_init(name_str);
*item_cnt = 0;
*file_idx = -1;
if(storage_dir_open(directory, string_get_cstr(path))) {
state = true;
while(1) {
if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) {
break;
}
if((storage_file_get_error(directory) == FSE_OK) && (name_temp[0] != '\0')) {
total_files_cnt++;
string_set_str(name_str, name_temp);
if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) {
if(!string_empty_p(filename)) {
if(string_cmp(name_str, filename) == 0) {
*file_idx = *item_cnt;
}
}
(*item_cnt)++;
}
if(total_files_cnt == LONG_LOAD_THRESHOLD) {
// There are too many files in folder and counting them will take some time - send callback to app
if(browser->long_load_cb) {
browser->long_load_cb(browser->cb_ctx);
}
}
}
}
}
string_clear(name_str);
storage_dir_close(directory);
storage_file_free(directory);
furi_record_close(RECORD_STORAGE);
return state;
}
static bool
browser_folder_load(BrowserWorker* browser, string_t path, uint32_t offset, uint32_t count) {
FileInfo file_info;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* directory = storage_file_alloc(storage);
char name_temp[FILE_NAME_LEN_MAX];
string_t name_str;
string_init(name_str);
uint32_t items_cnt = 0;
do {
if(!storage_dir_open(directory, string_get_cstr(path))) {
break;
}
items_cnt = 0;
while(items_cnt < offset) {
if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) {
break;
}
if(storage_file_get_error(directory) == FSE_OK) {
string_set_str(name_str, name_temp);
if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) {
items_cnt++;
}
} else {
break;
}
}
if(items_cnt != offset) {
break;
}
if(browser->list_load_cb) {
browser->list_load_cb(browser->cb_ctx, offset);
}
items_cnt = 0;
while(items_cnt < count) {
if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) {
break;
}
if(storage_file_get_error(directory) == FSE_OK) {
string_set_str(name_str, name_temp);
if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) {
string_printf(name_str, "%s/%s", string_get_cstr(path), name_temp);
if(browser->list_item_cb) {
browser->list_item_cb(
browser->cb_ctx, name_str, (file_info.flags & FSF_DIRECTORY), false);
}
items_cnt++;
}
} else {
break;
}
}
if(browser->list_item_cb) {
browser->list_item_cb(browser->cb_ctx, NULL, false, true);
}
} while(0);
string_clear(name_str);
storage_dir_close(directory);
storage_file_free(directory);
furi_record_close(RECORD_STORAGE);
return (items_cnt == count);
}
static int32_t browser_worker(void* context) {
BrowserWorker* browser = (BrowserWorker*)context;
furi_assert(browser);
FURI_LOG_D(TAG, "Start");
uint32_t items_cnt = 0;
string_t path;
string_init_set_str(path, BROWSER_ROOT);
browser->item_sel_idx = -1;
string_t filename;
string_init(filename);
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtConfigChange);
while(1) {
uint32_t flags =
furi_thread_flags_wait(WORKER_FLAGS_ALL, FuriFlagWaitAny, FuriWaitForever);
furi_assert((flags & FuriFlagError) == 0);
if(flags & WorkerEvtConfigChange) {
// If start path is a path to the file - try finding index of this file in a folder
if(browser_path_is_file(browser->path_next)) {
path_extract_filename(browser->path_next, filename, false);
}
idx_last_array_reset(browser->idx_last);
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderEnter);
}
if(flags & WorkerEvtFolderEnter) {
string_set(path, browser->path_next);
bool is_root = browser_folder_check_and_switch(path);
// Push previous selected item index to history array
idx_last_array_push_back(browser->idx_last, browser->item_sel_idx);
int32_t file_idx = 0;
browser_folder_init(browser, path, filename, &items_cnt, &file_idx);
FURI_LOG_D(
TAG,
"Enter folder: %s items: %u idx: %d",
string_get_cstr(path),
items_cnt,
file_idx);
if(browser->folder_cb) {
browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root);
}
string_reset(filename);
}
if(flags & WorkerEvtFolderExit) {
browser_path_trim(path);
bool is_root = browser_folder_check_and_switch(path);
int32_t file_idx = 0;
browser_folder_init(browser, path, filename, &items_cnt, &file_idx);
if(idx_last_array_size(browser->idx_last) > 0) {
// Pop previous selected item index from history array
idx_last_array_pop_back(&file_idx, browser->idx_last);
}
FURI_LOG_D(
TAG, "Exit to: %s items: %u idx: %d", string_get_cstr(path), items_cnt, file_idx);
if(browser->folder_cb) {
browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root);
}
}
if(flags & WorkerEvtFolderRefresh) {
bool is_root = browser_folder_check_and_switch(path);
int32_t file_idx = 0;
string_reset(filename);
browser_folder_init(browser, path, filename, &items_cnt, &file_idx);
FURI_LOG_D(
TAG,
"Refresh folder: %s items: %u idx: %d",
string_get_cstr(path),
items_cnt,
browser->item_sel_idx);
if(browser->folder_cb) {
browser->folder_cb(browser->cb_ctx, items_cnt, browser->item_sel_idx, is_root);
}
}
if(flags & WorkerEvtLoad) {
FURI_LOG_D(TAG, "Load offset: %u cnt: %u", browser->load_offset, browser->load_count);
browser_folder_load(browser, path, browser->load_offset, browser->load_count);
}
if(flags & WorkerEvtStop) {
break;
}
}
string_clear(filename);
string_clear(path);
FURI_LOG_D(TAG, "End");
return 0;
}
BrowserWorker* file_browser_worker_alloc(string_t path, const char* filter_ext, bool skip_assets) {
BrowserWorker* browser = malloc(sizeof(BrowserWorker));
idx_last_array_init(browser->idx_last);
string_init_set_str(browser->filter_extension, filter_ext);
browser->skip_assets = skip_assets;
string_init_set(browser->path_next, path);
browser->thread = furi_thread_alloc();
furi_thread_set_name(browser->thread, "BrowserWorker");
furi_thread_set_stack_size(browser->thread, 2048);
furi_thread_set_context(browser->thread, browser);
furi_thread_set_callback(browser->thread, browser_worker);
furi_thread_start(browser->thread);
return browser;
}
void file_browser_worker_free(BrowserWorker* browser) {
furi_assert(browser);
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtStop);
furi_thread_join(browser->thread);
furi_thread_free(browser->thread);
string_clear(browser->filter_extension);
string_clear(browser->path_next);
idx_last_array_clear(browser->idx_last);
free(browser);
}
void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context) {
furi_assert(browser);
browser->cb_ctx = context;
}
void file_browser_worker_set_folder_callback(
BrowserWorker* browser,
BrowserWorkerFolderOpenCallback cb) {
furi_assert(browser);
browser->folder_cb = cb;
}
void file_browser_worker_set_list_callback(
BrowserWorker* browser,
BrowserWorkerListLoadCallback cb) {
furi_assert(browser);
browser->list_load_cb = cb;
}
void file_browser_worker_set_item_callback(
BrowserWorker* browser,
BrowserWorkerListItemCallback cb) {
furi_assert(browser);
browser->list_item_cb = cb;
}
void file_browser_worker_set_long_load_callback(
BrowserWorker* browser,
BrowserWorkerLongLoadCallback cb) {
furi_assert(browser);
browser->long_load_cb = cb;
}
void file_browser_worker_set_config(
BrowserWorker* browser,
string_t path,
const char* filter_ext,
bool skip_assets) {
furi_assert(browser);
string_set(browser->path_next, path);
string_set_str(browser->filter_extension, filter_ext);
browser->skip_assets = skip_assets;
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtConfigChange);
}
void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx) {
furi_assert(browser);
string_set(browser->path_next, path);
browser->item_sel_idx = item_idx;
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderEnter);
}
void file_browser_worker_folder_exit(BrowserWorker* browser) {
furi_assert(browser);
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderExit);
}
void file_browser_worker_folder_refresh(BrowserWorker* browser, int32_t item_idx) {
furi_assert(browser);
browser->item_sel_idx = item_idx;
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtFolderRefresh);
}
void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count) {
furi_assert(browser);
browser->load_offset = offset;
browser->load_count = count;
furi_thread_flags_set(furi_thread_get_id(browser->thread), WorkerEvtLoad);
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include "m-string.h"
#include <gui/view.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct BrowserWorker BrowserWorker;
typedef void (*BrowserWorkerFolderOpenCallback)(
void* context,
uint32_t item_cnt,
int32_t file_idx,
bool is_root);
typedef void (*BrowserWorkerListLoadCallback)(void* context, uint32_t list_load_offset);
typedef void (*BrowserWorkerListItemCallback)(
void* context,
string_t item_path,
bool is_folder,
bool is_last);
typedef void (*BrowserWorkerLongLoadCallback)(void* context);
BrowserWorker* file_browser_worker_alloc(string_t path, const char* filter_ext, bool skip_assets);
void file_browser_worker_free(BrowserWorker* browser);
void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context);
void file_browser_worker_set_folder_callback(
BrowserWorker* browser,
BrowserWorkerFolderOpenCallback cb);
void file_browser_worker_set_list_callback(
BrowserWorker* browser,
BrowserWorkerListLoadCallback cb);
void file_browser_worker_set_item_callback(
BrowserWorker* browser,
BrowserWorkerListItemCallback cb);
void file_browser_worker_set_long_load_callback(
BrowserWorker* browser,
BrowserWorkerLongLoadCallback cb);
void file_browser_worker_set_config(
BrowserWorker* browser,
string_t path,
const char* filter_ext,
bool skip_assets);
void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx);
void file_browser_worker_folder_exit(BrowserWorker* browser);
void file_browser_worker_folder_refresh(BrowserWorker* browser, int32_t item_idx);
void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,94 @@
#include <stdint.h>
#include <furi.h>
#include <assets_icons.h>
#include <gui/icon_animation.h>
#include <gui/elements.h>
#include <gui/canvas.h>
#include <gui/view.h>
#include <input/input.h>
#include "loading.h"
struct Loading {
View* view;
};
typedef struct {
IconAnimation* icon;
} LoadingModel;
static void loading_draw_callback(Canvas* canvas, void* _model) {
LoadingModel* model = (LoadingModel*)_model;
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 0, 0, canvas_width(canvas), canvas_height(canvas));
canvas_set_color(canvas, ColorBlack);
uint8_t x = canvas_width(canvas) / 2 - 24 / 2;
uint8_t y = canvas_height(canvas) / 2 - 24 / 2;
canvas_draw_icon(canvas, x, y, &A_Loading_24);
canvas_draw_icon_animation(canvas, x, y, model->icon);
}
static bool loading_input_callback(InputEvent* event, void* context) {
UNUSED(event);
furi_assert(context);
return true;
}
static void loading_enter_callback(void* context) {
furi_assert(context);
Loading* instance = context;
LoadingModel* model = view_get_model(instance->view);
/* using Loading View in conjunction with several
* Stack View obligates to reassign
* Update callback, as it can be rewritten
*/
view_tie_icon_animation(instance->view, model->icon);
icon_animation_start(model->icon);
view_commit_model(instance->view, false);
}
static void loading_exit_callback(void* context) {
furi_assert(context);
Loading* instance = context;
LoadingModel* model = view_get_model(instance->view);
icon_animation_stop(model->icon);
view_commit_model(instance->view, false);
}
Loading* loading_alloc(void) {
Loading* instance = malloc(sizeof(Loading));
instance->view = view_alloc();
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(LoadingModel));
LoadingModel* model = view_get_model(instance->view);
model->icon = icon_animation_alloc(&A_Loading_24);
view_tie_icon_animation(instance->view, model->icon);
view_commit_model(instance->view, false);
view_set_context(instance->view, instance);
view_set_draw_callback(instance->view, loading_draw_callback);
view_set_input_callback(instance->view, loading_input_callback);
view_set_enter_callback(instance->view, loading_enter_callback);
view_set_exit_callback(instance->view, loading_exit_callback);
return instance;
}
void loading_free(Loading* instance) {
LoadingModel* model = view_get_model(instance->view);
icon_animation_free(model->icon);
view_commit_model(instance->view, false);
furi_assert(instance);
view_free(instance->view);
free(instance);
}
View* loading_get_view(Loading* instance) {
furi_assert(instance);
furi_assert(instance->view);
return instance->view;
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Loading anonymous structure */
typedef struct Loading Loading;
/** Allocate and initialize
*
* This View used to show system is doing some processing
*
* @return Loading View instance
*/
Loading* loading_alloc();
/** Deinitialize and free Loading View
*
* @param instance Loading instance
*/
void loading_free(Loading* instance);
/** Get Loading view
*
* @param instance Loading instance
*
* @return View instance that can be used for embedding
*/
View* loading_get_view(Loading* instance);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,267 @@
#include "menu.h"
#include <m-array.h>
#include <gui/elements.h>
#include <furi.h>
struct Menu {
View* view;
};
typedef struct {
const char* label;
IconAnimation* icon;
uint32_t index;
MenuItemCallback callback;
void* callback_context;
} MenuItem;
ARRAY_DEF(MenuItemArray, MenuItem, M_POD_OPLIST);
#define M_OPL_MenuItemArray_t() ARRAY_OPLIST(MenuItemArray, M_POD_OPLIST)
typedef struct {
MenuItemArray_t items;
size_t position;
} MenuModel;
static void menu_process_up(Menu* menu);
static void menu_process_down(Menu* menu);
static void menu_process_ok(Menu* menu);
static void menu_draw_callback(Canvas* canvas, void* _model) {
MenuModel* model = _model;
canvas_clear(canvas);
size_t position = model->position;
size_t items_count = MenuItemArray_size(model->items);
if(items_count) {
MenuItem* item;
size_t shift_position;
// First line
canvas_set_font(canvas, FontSecondary);
shift_position = (0 + position + items_count - 1) % items_count;
item = MenuItemArray_get(model->items, shift_position);
if(item->icon) {
canvas_draw_icon_animation(canvas, 4, 3, item->icon);
}
canvas_draw_str(canvas, 22, 14, item->label);
// Second line main
canvas_set_font(canvas, FontPrimary);
shift_position = (1 + position + items_count - 1) % items_count;
item = MenuItemArray_get(model->items, shift_position);
if(item->icon) {
canvas_draw_icon_animation(canvas, 4, 25, item->icon);
}
canvas_draw_str(canvas, 22, 36, item->label);
// Third line
canvas_set_font(canvas, FontSecondary);
shift_position = (2 + position + items_count - 1) % items_count;
item = MenuItemArray_get(model->items, shift_position);
if(item->icon) {
canvas_draw_icon_animation(canvas, 4, 47, item->icon);
}
canvas_draw_str(canvas, 22, 58, item->label);
// Frame and scrollbar
elements_frame(canvas, 0, 21, 128 - 5, 21);
elements_scrollbar(canvas, position, items_count);
} else {
canvas_draw_str(canvas, 2, 32, "Empty");
elements_scrollbar(canvas, 0, 0);
}
}
static bool menu_input_callback(InputEvent* event, void* context) {
Menu* menu = context;
bool consumed = false;
if(event->type == InputTypeShort) {
if(event->key == InputKeyUp) {
consumed = true;
menu_process_up(menu);
} else if(event->key == InputKeyDown) {
consumed = true;
menu_process_down(menu);
} else if(event->key == InputKeyOk) {
consumed = true;
menu_process_ok(menu);
}
} else if(event->type == InputTypeRepeat) {
if(event->key == InputKeyUp) {
consumed = true;
menu_process_up(menu);
} else if(event->key == InputKeyDown) {
consumed = true;
menu_process_down(menu);
}
}
return consumed;
}
static void menu_enter(void* context) {
Menu* menu = context;
with_view_model(
menu->view, (MenuModel * model) {
MenuItem* item = MenuItemArray_get(model->items, model->position);
if(item && item->icon) {
icon_animation_start(item->icon);
}
return false;
});
}
static void menu_exit(void* context) {
Menu* menu = context;
with_view_model(
menu->view, (MenuModel * model) {
MenuItem* item = MenuItemArray_get(model->items, model->position);
if(item && item->icon) {
icon_animation_stop(item->icon);
}
return false;
});
}
Menu* menu_alloc() {
Menu* menu = malloc(sizeof(Menu));
menu->view = view_alloc(menu->view);
view_set_context(menu->view, menu);
view_allocate_model(menu->view, ViewModelTypeLocking, sizeof(MenuModel));
view_set_draw_callback(menu->view, menu_draw_callback);
view_set_input_callback(menu->view, menu_input_callback);
view_set_enter_callback(menu->view, menu_enter);
view_set_exit_callback(menu->view, menu_exit);
with_view_model(
menu->view, (MenuModel * model) {
MenuItemArray_init(model->items);
model->position = 0;
return true;
});
return menu;
}
void menu_free(Menu* menu) {
furi_assert(menu);
menu_reset(menu);
view_free(menu->view);
free(menu);
}
View* menu_get_view(Menu* menu) {
furi_assert(menu);
return (menu->view);
}
void menu_add_item(
Menu* menu,
const char* label,
const Icon* icon,
uint32_t index,
MenuItemCallback callback,
void* context) {
furi_assert(menu);
furi_assert(label);
MenuItem* item = NULL;
with_view_model(
menu->view, (MenuModel * model) {
item = MenuItemArray_push_new(model->items);
item->label = label;
item->icon = icon ? icon_animation_alloc(icon) : icon_animation_alloc(&A_Plugins_14);
view_tie_icon_animation(menu->view, item->icon);
item->index = index;
item->callback = callback;
item->callback_context = context;
return true;
});
}
void menu_reset(Menu* menu) {
furi_assert(menu);
with_view_model(
menu->view, (MenuModel * model) {
for
M_EACH(item, model->items, MenuItemArray_t) {
icon_animation_stop(item->icon);
icon_animation_free(item->icon);
}
MenuItemArray_reset(model->items);
model->position = 0;
return true;
});
}
void menu_set_selected_item(Menu* menu, uint32_t index) {
with_view_model(
menu->view, (MenuModel * model) {
if(index >= MenuItemArray_size(model->items)) {
return false;
}
model->position = index;
return true;
});
}
static void menu_process_up(Menu* menu) {
with_view_model(
menu->view, (MenuModel * model) {
MenuItem* item = MenuItemArray_get(model->items, model->position);
if(item && item->icon) {
icon_animation_stop(item->icon);
}
if(model->position > 0) {
model->position--;
} else {
model->position = MenuItemArray_size(model->items) - 1;
}
item = MenuItemArray_get(model->items, model->position);
if(item && item->icon) {
icon_animation_start(item->icon);
}
return true;
});
}
static void menu_process_down(Menu* menu) {
with_view_model(
menu->view, (MenuModel * model) {
MenuItem* item = MenuItemArray_get(model->items, model->position);
if(item && item->icon) {
icon_animation_stop(item->icon);
}
if(model->position < MenuItemArray_size(model->items) - 1) {
model->position++;
} else {
model->position = 0;
}
item = MenuItemArray_get(model->items, model->position);
if(item && item->icon) {
icon_animation_start(item->icon);
}
return true;
});
}
static void menu_process_ok(Menu* menu) {
MenuItem* item = NULL;
with_view_model(
menu->view, (MenuModel * model) {
if(model->position < MenuItemArray_size(model->items)) {
item = MenuItemArray_get(model->items, model->position);
}
return true;
});
if(item && item->callback) {
item->callback(item->callback_context, item->index);
}
}

View File

@@ -0,0 +1,73 @@
/**
* @file menu.h
* GUI: Menu view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Menu anonymous structure */
typedef struct Menu Menu;
/** Menu Item Callback */
typedef void (*MenuItemCallback)(void* context, uint32_t index);
/** Menu allocation and initialization
*
* @return Menu instance
*/
Menu* menu_alloc();
/** Free menu
*
* @param menu Menu instance
*/
void menu_free(Menu* menu);
/** Get Menu view
*
* @param menu Menu instance
*
* @return View instance
*/
View* menu_get_view(Menu* menu);
/** Add item to menu
*
* @param menu Menu instance
* @param label menu item string label
* @param icon IconAnimation instance
* @param index menu item index
* @param callback MenuItemCallback instance
* @param context pointer to context
*/
void menu_add_item(
Menu* menu,
const char* label,
const Icon* icon,
uint32_t index,
MenuItemCallback callback,
void* context);
/** Clean menu
* @note this function does not free menu instance
*
* @param menu Menu instance
*/
void menu_reset(Menu* menu);
/** Set current menu item
*
* @param menu Menu instance
* @param index The index
*/
void menu_set_selected_item(Menu* menu, uint32_t index);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,246 @@
#include "popup.h"
#include <gui/elements.h>
#include <furi.h>
struct Popup {
View* view;
void* context;
PopupCallback callback;
FuriTimer* timer;
uint32_t timer_period_in_ms;
bool timer_enabled;
};
typedef struct {
const char* text;
uint8_t x;
uint8_t y;
Align horizontal;
Align vertical;
} TextElement;
typedef struct {
uint8_t x;
uint8_t y;
const Icon* icon;
} IconElement;
typedef struct {
TextElement header;
TextElement text;
IconElement icon;
} PopupModel;
static void popup_view_draw_callback(Canvas* canvas, void* _model) {
PopupModel* model = _model;
// Prepare canvas
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
if(model->icon.icon != NULL) {
canvas_draw_icon(canvas, model->icon.x, model->icon.y, model->icon.icon);
}
// Draw header
if(model->header.text != NULL) {
canvas_set_font(canvas, FontPrimary);
elements_multiline_text_aligned(
canvas,
model->header.x,
model->header.y,
model->header.horizontal,
model->header.vertical,
model->header.text);
}
// Draw text
if(model->text.text != NULL) {
canvas_set_font(canvas, FontSecondary);
elements_multiline_text_aligned(
canvas,
model->text.x,
model->text.y,
model->text.horizontal,
model->text.vertical,
model->text.text);
}
}
static void popup_timer_callback(void* context) {
furi_assert(context);
Popup* popup = context;
if(popup->callback) {
popup->callback(popup->context);
}
}
static bool popup_view_input_callback(InputEvent* event, void* context) {
Popup* popup = context;
bool consumed = false;
// Process key presses only
if(event->type == InputTypeShort && popup->callback) {
popup->callback(popup->context);
consumed = true;
}
return consumed;
}
void popup_start_timer(void* context) {
Popup* popup = context;
if(popup->timer_enabled) {
uint32_t timer_period =
popup->timer_period_in_ms / (1000.0f / furi_kernel_get_tick_frequency());
if(timer_period == 0) timer_period = 1;
if(furi_timer_start(popup->timer, timer_period) != FuriStatusOk) {
furi_assert(0);
};
}
}
void popup_stop_timer(void* context) {
Popup* popup = context;
furi_timer_stop(popup->timer);
}
Popup* popup_alloc() {
Popup* popup = malloc(sizeof(Popup));
popup->view = view_alloc();
popup->timer = furi_timer_alloc(popup_timer_callback, FuriTimerTypeOnce, popup);
furi_assert(popup->timer);
popup->timer_period_in_ms = 1000;
popup->timer_enabled = false;
view_set_context(popup->view, popup);
view_allocate_model(popup->view, ViewModelTypeLockFree, sizeof(PopupModel));
view_set_draw_callback(popup->view, popup_view_draw_callback);
view_set_input_callback(popup->view, popup_view_input_callback);
view_set_enter_callback(popup->view, popup_start_timer);
view_set_exit_callback(popup->view, popup_stop_timer);
with_view_model(
popup->view, (PopupModel * model) {
model->header.text = NULL;
model->header.x = 0;
model->header.y = 0;
model->header.horizontal = AlignLeft;
model->header.vertical = AlignBottom;
model->text.text = NULL;
model->text.x = 0;
model->text.y = 0;
model->text.horizontal = AlignLeft;
model->text.vertical = AlignBottom;
model->icon.x = 0;
model->icon.y = 0;
model->icon.icon = NULL;
return true;
});
return popup;
}
void popup_free(Popup* popup) {
furi_assert(popup);
furi_timer_free(popup->timer);
view_free(popup->view);
free(popup);
}
View* popup_get_view(Popup* popup) {
furi_assert(popup);
return popup->view;
}
void popup_set_callback(Popup* popup, PopupCallback callback) {
furi_assert(popup);
popup->callback = callback;
}
void popup_set_context(Popup* popup, void* context) {
furi_assert(popup);
popup->context = context;
}
void popup_set_header(
Popup* popup,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical) {
furi_assert(popup);
with_view_model(
popup->view, (PopupModel * model) {
model->header.text = text;
model->header.x = x;
model->header.y = y;
model->header.horizontal = horizontal;
model->header.vertical = vertical;
return true;
});
}
void popup_set_text(
Popup* popup,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical) {
furi_assert(popup);
with_view_model(
popup->view, (PopupModel * model) {
model->text.text = text;
model->text.x = x;
model->text.y = y;
model->text.horizontal = horizontal;
model->text.vertical = vertical;
return true;
});
}
void popup_set_icon(Popup* popup, uint8_t x, uint8_t y, const Icon* icon) {
furi_assert(popup);
with_view_model(
popup->view, (PopupModel * model) {
model->icon.x = x;
model->icon.y = y;
model->icon.icon = icon;
return true;
});
}
void popup_set_timeout(Popup* popup, uint32_t timeout_in_ms) {
furi_assert(popup);
popup->timer_period_in_ms = timeout_in_ms;
}
void popup_enable_timeout(Popup* popup) {
popup->timer_enabled = true;
}
void popup_disable_timeout(Popup* popup) {
popup->timer_enabled = false;
}
void popup_reset(Popup* popup) {
furi_assert(popup);
with_view_model(
popup->view, (PopupModel * model) {
memset(&model->header, 0, sizeof(model->header));
memset(&model->text, 0, sizeof(model->text));
memset(&model->icon, 0, sizeof(model->icon));
return false;
});
popup->callback = NULL;
popup->context = NULL;
popup->timer_enabled = false;
popup->timer_period_in_ms = 0;
}

View File

@@ -0,0 +1,134 @@
/**
* @file popup.h
* GUI: Popup view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Popup anonymous structure */
typedef struct Popup Popup;
/** Popup result callback type
* @warning comes from GUI thread
*/
typedef void (*PopupCallback)(void* context);
/** Allocate and initialize popup
*
* This popup used to ask simple questions like Yes/
*
* @return Popup instance
*/
Popup* popup_alloc();
/** Deinitialize and free popup
*
* @param popup Popup instance
*/
void popup_free(Popup* popup);
/** Get popup view
*
* @param popup Popup instance
*
* @return View instance that can be used for embedding
*/
View* popup_get_view(Popup* popup);
/** Set popup header text
*
* @param popup Popup instance
* @param callback PopupCallback
*/
void popup_set_callback(Popup* popup, PopupCallback callback);
/** Set popup context
*
* @param popup Popup instance
* @param context context pointer, will be passed to result callback
*/
void popup_set_context(Popup* popup, void* context);
/** Set popup header text
*
* If text is null, popup header will not be rendered
*
* @param popup Popup instance
* @param text text to be shown, can be multiline
* @param x x position
* @param y y position
* @param horizontal horizontal alignment
* @param vertical vertical aligment
*/
void popup_set_header(
Popup* popup,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical);
/** Set popup text
*
* If text is null, popup text will not be rendered
*
* @param popup Popup instance
* @param text text to be shown, can be multiline
* @param x x position
* @param y y position
* @param horizontal horizontal alignment
* @param vertical vertical aligment
*/
void popup_set_text(
Popup* popup,
const char* text,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical);
/** Set popup icon
*
* If icon position is negative, popup icon will not be rendered
*
* @param popup Popup instance
* @param x x position
* @param y y position
* @param icon pointer to Icon data
*/
void popup_set_icon(Popup* popup, uint8_t x, uint8_t y, const Icon* icon);
/** Set popup timeout
*
* @param popup Popup instance
* @param timeout_in_ms popup timeout value in milliseconds
*/
void popup_set_timeout(Popup* popup, uint32_t timeout_in_ms);
/** Enable popup timeout
*
* @param popup Popup instance
*/
void popup_enable_timeout(Popup* popup);
/** Disable popup timeout
*
* @param popup Popup instance
*/
void popup_disable_timeout(Popup* popup);
/** Reset popup instance state
*
* @param popup Popup instance
*/
void popup_reset(Popup* popup);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,296 @@
#include "submenu.h"
#include <m-array.h>
#include <gui/elements.h>
#include <furi.h>
struct Submenu {
View* view;
};
typedef struct {
const char* label;
uint32_t index;
SubmenuItemCallback callback;
void* callback_context;
} SubmenuItem;
ARRAY_DEF(SubmenuItemArray, SubmenuItem, M_POD_OPLIST);
typedef struct {
SubmenuItemArray_t items;
const char* header;
uint8_t position;
uint8_t window_position;
} SubmenuModel;
static void submenu_process_up(Submenu* submenu);
static void submenu_process_down(Submenu* submenu);
static void submenu_process_ok(Submenu* submenu);
static void submenu_view_draw_callback(Canvas* canvas, void* _model) {
SubmenuModel* model = _model;
const uint8_t item_height = 16;
const uint8_t item_width = 123;
canvas_clear(canvas);
uint8_t position = 0;
SubmenuItemArray_it_t it;
if(model->header) {
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 4, 11, model->header);
}
canvas_set_font(canvas, FontSecondary);
for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
SubmenuItemArray_next(it)) {
uint8_t item_position = position - model->window_position;
uint8_t items_on_screen = model->header ? 3 : 4;
uint8_t y_offset = model->header ? 16 : 0;
if(item_position < items_on_screen) {
if(position == model->position) {
canvas_set_color(canvas, ColorBlack);
elements_slightly_rounded_box(
canvas,
0,
y_offset + (item_position * item_height) + 1,
item_width,
item_height - 2);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_set_color(canvas, ColorBlack);
}
string_t disp_str;
string_init_set_str(disp_str, SubmenuItemArray_cref(it)->label);
elements_string_fit_width(canvas, disp_str, item_width - 20);
canvas_draw_str(
canvas,
6,
y_offset + (item_position * item_height) + item_height - 4,
string_get_cstr(disp_str));
string_clear(disp_str);
}
position++;
}
elements_scrollbar(canvas, model->position, SubmenuItemArray_size(model->items));
}
static bool submenu_view_input_callback(InputEvent* event, void* context) {
Submenu* submenu = context;
furi_assert(submenu);
bool consumed = false;
if(event->type == InputTypeShort) {
switch(event->key) {
case InputKeyUp:
consumed = true;
submenu_process_up(submenu);
break;
case InputKeyDown:
consumed = true;
submenu_process_down(submenu);
break;
case InputKeyOk:
consumed = true;
submenu_process_ok(submenu);
break;
default:
break;
}
} else if(event->type == InputTypeRepeat) {
if(event->key == InputKeyUp) {
consumed = true;
submenu_process_up(submenu);
} else if(event->key == InputKeyDown) {
consumed = true;
submenu_process_down(submenu);
}
}
return consumed;
}
Submenu* submenu_alloc() {
Submenu* submenu = malloc(sizeof(Submenu));
submenu->view = view_alloc();
view_set_context(submenu->view, submenu);
view_allocate_model(submenu->view, ViewModelTypeLocking, sizeof(SubmenuModel));
view_set_draw_callback(submenu->view, submenu_view_draw_callback);
view_set_input_callback(submenu->view, submenu_view_input_callback);
with_view_model(
submenu->view, (SubmenuModel * model) {
SubmenuItemArray_init(model->items);
model->position = 0;
model->window_position = 0;
model->header = NULL;
return true;
});
return submenu;
}
void submenu_free(Submenu* submenu) {
furi_assert(submenu);
with_view_model(
submenu->view, (SubmenuModel * model) {
SubmenuItemArray_clear(model->items);
return true;
});
view_free(submenu->view);
free(submenu);
}
View* submenu_get_view(Submenu* submenu) {
furi_assert(submenu);
return submenu->view;
}
void submenu_add_item(
Submenu* submenu,
const char* label,
uint32_t index,
SubmenuItemCallback callback,
void* callback_context) {
SubmenuItem* item = NULL;
furi_assert(label);
furi_assert(submenu);
with_view_model(
submenu->view, (SubmenuModel * model) {
item = SubmenuItemArray_push_new(model->items);
item->label = label;
item->index = index;
item->callback = callback;
item->callback_context = callback_context;
return true;
});
}
void submenu_reset(Submenu* submenu) {
furi_assert(submenu);
with_view_model(
submenu->view, (SubmenuModel * model) {
SubmenuItemArray_reset(model->items);
model->position = 0;
model->window_position = 0;
model->header = NULL;
return true;
});
}
void submenu_set_selected_item(Submenu* submenu, uint32_t index) {
with_view_model(
submenu->view, (SubmenuModel * model) {
uint32_t position = 0;
SubmenuItemArray_it_t it;
for(SubmenuItemArray_it(it, model->items); !SubmenuItemArray_end_p(it);
SubmenuItemArray_next(it)) {
if(index == SubmenuItemArray_cref(it)->index) {
break;
}
position++;
}
if(position >= SubmenuItemArray_size(model->items)) {
position = 0;
}
model->position = position;
model->window_position = position;
if(model->window_position > 0) {
model->window_position -= 1;
}
uint8_t items_on_screen = model->header ? 3 : 4;
if(SubmenuItemArray_size(model->items) <= items_on_screen) {
model->window_position = 0;
} else {
if(model->window_position >=
(SubmenuItemArray_size(model->items) - items_on_screen)) {
model->window_position =
(SubmenuItemArray_size(model->items) - items_on_screen);
}
}
return true;
});
}
void submenu_process_up(Submenu* submenu) {
with_view_model(
submenu->view, (SubmenuModel * model) {
uint8_t items_on_screen = model->header ? 3 : 4;
if(model->position > 0) {
model->position--;
if(((model->position - model->window_position) < 1) &&
model->window_position > 0) {
model->window_position--;
}
} else {
model->position = SubmenuItemArray_size(model->items) - 1;
if(model->position > (items_on_screen - 1)) {
model->window_position = model->position - (items_on_screen - 1);
}
}
return true;
});
}
void submenu_process_down(Submenu* submenu) {
with_view_model(
submenu->view, (SubmenuModel * model) {
uint8_t items_on_screen = model->header ? 3 : 4;
if(model->position < (SubmenuItemArray_size(model->items) - 1)) {
model->position++;
if((model->position - model->window_position) > (items_on_screen - 2) &&
model->window_position <
(SubmenuItemArray_size(model->items) - items_on_screen)) {
model->window_position++;
}
} else {
model->position = 0;
model->window_position = 0;
}
return true;
});
}
void submenu_process_ok(Submenu* submenu) {
SubmenuItem* item = NULL;
with_view_model(
submenu->view, (SubmenuModel * model) {
if(model->position < (SubmenuItemArray_size(model->items))) {
item = SubmenuItemArray_get(model->items, model->position);
}
return true;
});
if(item && item->callback) {
item->callback(item->callback_context, item->index);
}
}
void submenu_set_header(Submenu* submenu, const char* header) {
furi_assert(submenu);
with_view_model(
submenu->view, (SubmenuModel * model) {
model->header = header;
return true;
});
}

View File

@@ -0,0 +1,78 @@
/**
* @file submenu.h
* GUI: SubMenu view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Submenu anonymous structure */
typedef struct Submenu Submenu;
typedef void (*SubmenuItemCallback)(void* context, uint32_t index);
/** Allocate and initialize submenu
*
* This submenu is used to select one option
*
* @return Submenu instance
*/
Submenu* submenu_alloc();
/** Deinitialize and free submenu
*
* @param submenu Submenu instance
*/
void submenu_free(Submenu* submenu);
/** Get submenu view
*
* @param submenu Submenu instance
*
* @return View instance that can be used for embedding
*/
View* submenu_get_view(Submenu* submenu);
/** Add item to submenu
*
* @param submenu Submenu instance
* @param label menu item label
* @param index menu item index, used for callback, may be
* the same with other items
* @param callback menu item callback
* @param callback_context menu item callback context
*/
void submenu_add_item(
Submenu* submenu,
const char* label,
uint32_t index,
SubmenuItemCallback callback,
void* callback_context);
/** Remove all items from submenu
*
* @param submenu Submenu instance
*/
void submenu_reset(Submenu* submenu);
/** Set submenu item selector
*
* @param submenu Submenu instance
* @param index The index
*/
void submenu_set_selected_item(Submenu* submenu, uint32_t index);
/** Set optional header for submenu
*
* @param submenu Submenu instance
* @param header header to set
*/
void submenu_set_header(Submenu* submenu, const char* header);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,214 @@
#include "text_box.h"
#include "gui/canvas.h"
#include <m-string.h>
#include <furi.h>
#include <gui/elements.h>
#include <stdint.h>
struct TextBox {
View* view;
};
typedef struct {
const char* text;
char* text_pos;
string_t text_formatted;
int32_t scroll_pos;
int32_t scroll_num;
TextBoxFont font;
TextBoxFocus focus;
bool formatted;
} TextBoxModel;
static void text_box_process_down(TextBox* text_box) {
with_view_model(
text_box->view, (TextBoxModel * model) {
if(model->scroll_pos < model->scroll_num - 1) {
model->scroll_pos++;
// Search next line start
while(*model->text_pos++ != '\n')
;
}
return true;
});
}
static void text_box_process_up(TextBox* text_box) {
with_view_model(
text_box->view, (TextBoxModel * model) {
if(model->scroll_pos > 0) {
model->scroll_pos--;
// Reach last symbol of previous line
model->text_pos--;
// Search prevous line start
while((model->text_pos != model->text) && (*(--model->text_pos) != '\n'))
;
if(*model->text_pos == '\n') {
model->text_pos++;
}
}
return true;
});
}
static void text_box_insert_endline(Canvas* canvas, TextBoxModel* model) {
size_t i = 0;
size_t line_width = 0;
const char* str = model->text;
size_t line_num = 0;
const size_t text_width = 120;
while(str[i] != '\0') {
char symb = str[i++];
if(symb != '\n') {
size_t glyph_width = canvas_glyph_width(canvas, symb);
if(line_width + glyph_width > text_width) {
line_num++;
line_width = 0;
string_push_back(model->text_formatted, '\n');
}
line_width += glyph_width;
} else {
line_num++;
line_width = 0;
}
string_push_back(model->text_formatted, symb);
}
line_num++;
model->text = string_get_cstr(model->text_formatted);
model->text_pos = (char*)model->text;
if(model->focus == TextBoxFocusEnd && line_num > 5) {
// Set text position to 5th line from the end
for(uint8_t i = 0; i < line_num - 5; i++) {
while(*model->text_pos++ != '\n') {
};
}
model->scroll_num = line_num - 4;
model->scroll_pos = line_num - 5;
} else {
model->scroll_num = MAX(line_num - 4, 0u);
model->scroll_pos = 0;
}
}
static void text_box_view_draw_callback(Canvas* canvas, void* _model) {
TextBoxModel* model = _model;
canvas_clear(canvas);
if(model->font == TextBoxFontText) {
canvas_set_font(canvas, FontSecondary);
} else if(model->font == TextBoxFontHex) {
canvas_set_font(canvas, FontKeyboard);
}
if(!model->formatted) {
text_box_insert_endline(canvas, model);
model->formatted = true;
}
elements_slightly_rounded_frame(canvas, 0, 0, 124, 64);
elements_multiline_text(canvas, 3, 11, model->text_pos);
elements_scrollbar(canvas, model->scroll_pos, model->scroll_num);
}
static bool text_box_view_input_callback(InputEvent* event, void* context) {
furi_assert(context);
TextBox* text_box = context;
bool consumed = false;
if(event->type == InputTypeShort) {
if(event->key == InputKeyDown) {
text_box_process_down(text_box);
consumed = true;
} else if(event->key == InputKeyUp) {
text_box_process_up(text_box);
consumed = true;
}
}
return consumed;
}
TextBox* text_box_alloc() {
TextBox* text_box = malloc(sizeof(TextBox));
text_box->view = view_alloc();
view_set_context(text_box->view, text_box);
view_allocate_model(text_box->view, ViewModelTypeLocking, sizeof(TextBoxModel));
view_set_draw_callback(text_box->view, text_box_view_draw_callback);
view_set_input_callback(text_box->view, text_box_view_input_callback);
with_view_model(
text_box->view, (TextBoxModel * model) {
model->text = NULL;
string_init_set_str(model->text_formatted, "");
model->formatted = false;
model->font = TextBoxFontText;
return true;
});
return text_box;
}
void text_box_free(TextBox* text_box) {
furi_assert(text_box);
with_view_model(
text_box->view, (TextBoxModel * model) {
string_clear(model->text_formatted);
return true;
});
view_free(text_box->view);
free(text_box);
}
View* text_box_get_view(TextBox* text_box) {
furi_assert(text_box);
return text_box->view;
}
void text_box_reset(TextBox* text_box) {
furi_assert(text_box);
with_view_model(
text_box->view, (TextBoxModel * model) {
model->text = NULL;
string_set_str(model->text_formatted, "");
model->font = TextBoxFontText;
model->focus = TextBoxFocusStart;
return true;
});
}
void text_box_set_text(TextBox* text_box, const char* text) {
furi_assert(text_box);
furi_assert(text);
with_view_model(
text_box->view, (TextBoxModel * model) {
model->text = text;
string_reset(model->text_formatted);
string_reserve(model->text_formatted, strlen(text));
model->formatted = false;
return true;
});
}
void text_box_set_font(TextBox* text_box, TextBoxFont font) {
furi_assert(text_box);
with_view_model(
text_box->view, (TextBoxModel * model) {
model->font = font;
return true;
});
}
void text_box_set_focus(TextBox* text_box, TextBoxFocus focus) {
furi_assert(text_box);
with_view_model(
text_box->view, (TextBoxModel * model) {
model->focus = focus;
return true;
});
}

View File

@@ -0,0 +1,77 @@
/**
* @file text_box.h
* GUI: TextBox view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
/** TextBox anonymous structure */
typedef struct TextBox TextBox;
typedef enum {
TextBoxFontText,
TextBoxFontHex,
} TextBoxFont;
typedef enum {
TextBoxFocusStart,
TextBoxFocusEnd,
} TextBoxFocus;
/** Allocate and initialize text_box
*
* @return TextBox instance
*/
TextBox* text_box_alloc();
/** Deinitialize and free text_box
*
* @param text_box text_box instance
*/
void text_box_free(TextBox* text_box);
/** Get text_box view
*
* @param text_box TextBox instance
*
* @return View instance that can be used for embedding
*/
View* text_box_get_view(TextBox* text_box);
/** Clean text_box
*
* @param text_box TextBox instance
*/
void text_box_reset(TextBox* text_box);
/** Set text for text_box
*
* @param text_box TextBox instance
* @param text text to set
*/
void text_box_set_text(TextBox* text_box, const char* text);
/** Set TextBox font
*
* @param text_box TextBox instance
* @param font TextBoxFont instance
*/
void text_box_set_font(TextBox* text_box, TextBoxFont font);
/** Set TextBox focus
* @note Use to display from start or from end
*
* @param text_box TextBox instance
* @param focus TextBoxFocus instance
*/
void text_box_set_focus(TextBox* text_box, TextBoxFocus focus);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,561 @@
#include "text_input.h"
#include <gui/elements.h>
#include <furi.h>
struct TextInput {
View* view;
FuriTimer* timer;
};
typedef struct {
const char text;
const uint8_t x;
const uint8_t y;
} TextInputKey;
typedef struct {
const char* header;
char* text_buffer;
size_t text_buffer_size;
bool clear_default_text;
TextInputCallback callback;
void* callback_context;
uint8_t selected_row;
uint8_t selected_column;
TextInputValidatorCallback validator_callback;
void* validator_callback_context;
string_t validator_text;
bool valadator_message_visible;
} TextInputModel;
static const uint8_t keyboard_origin_x = 1;
static const uint8_t keyboard_origin_y = 29;
static const uint8_t keyboard_row_count = 3;
#define ENTER_KEY '\r'
#define BACKSPACE_KEY '\b'
static const TextInputKey keyboard_keys_row_1[] = {
{'q', 1, 8},
{'w', 10, 8},
{'e', 19, 8},
{'r', 28, 8},
{'t', 37, 8},
{'y', 46, 8},
{'u', 55, 8},
{'i', 64, 8},
{'o', 73, 8},
{'p', 82, 8},
{'0', 91, 8},
{'1', 100, 8},
{'2', 110, 8},
{'3', 120, 8},
};
static const TextInputKey keyboard_keys_row_2[] = {
{'a', 1, 20},
{'s', 10, 20},
{'d', 19, 20},
{'f', 28, 20},
{'g', 37, 20},
{'h', 46, 20},
{'j', 55, 20},
{'k', 64, 20},
{'l', 73, 20},
{BACKSPACE_KEY, 82, 12},
{'4', 100, 20},
{'5', 110, 20},
{'6', 120, 20},
};
static const TextInputKey keyboard_keys_row_3[] = {
{'z', 1, 32},
{'x', 10, 32},
{'c', 19, 32},
{'v', 28, 32},
{'b', 37, 32},
{'n', 46, 32},
{'m', 55, 32},
{'_', 64, 32},
{ENTER_KEY, 74, 23},
{'7', 100, 32},
{'8', 110, 32},
{'9', 120, 32},
};
static uint8_t get_row_size(uint8_t row_index) {
uint8_t row_size = 0;
switch(row_index + 1) {
case 1:
row_size = sizeof(keyboard_keys_row_1) / sizeof(TextInputKey);
break;
case 2:
row_size = sizeof(keyboard_keys_row_2) / sizeof(TextInputKey);
break;
case 3:
row_size = sizeof(keyboard_keys_row_3) / sizeof(TextInputKey);
break;
}
return row_size;
}
static const TextInputKey* get_row(uint8_t row_index) {
const TextInputKey* row = NULL;
switch(row_index + 1) {
case 1:
row = keyboard_keys_row_1;
break;
case 2:
row = keyboard_keys_row_2;
break;
case 3:
row = keyboard_keys_row_3;
break;
}
return row;
}
static char get_selected_char(TextInputModel* model) {
return get_row(model->selected_row)[model->selected_column].text;
}
static bool char_is_lowercase(char letter) {
return (letter >= 0x61 && letter <= 0x7A);
}
static char char_to_uppercase(const char letter) {
if(letter == '_') {
return 0x20;
} else if(isalpha(letter)) {
return (letter - 0x20);
} else {
return letter;
}
}
static void text_input_backspace_cb(TextInputModel* model) {
uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
if(text_length > 0) {
model->text_buffer[text_length - 1] = 0;
}
}
static void text_input_view_draw_callback(Canvas* canvas, void* _model) {
TextInputModel* model = _model;
uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
uint8_t needed_string_width = canvas_width(canvas) - 8;
uint8_t start_pos = 4;
const char* text = model->text_buffer;
canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
canvas_draw_str(canvas, 2, 8, model->header);
elements_slightly_rounded_frame(canvas, 1, 12, 126, 15);
if(canvas_string_width(canvas, text) > needed_string_width) {
canvas_draw_str(canvas, start_pos, 22, "...");
start_pos += 6;
needed_string_width -= 8;
}
while(text != 0 && canvas_string_width(canvas, text) > needed_string_width) {
text++;
}
if(model->clear_default_text) {
elements_slightly_rounded_box(
canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 22, "|");
canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 22, "|");
}
canvas_draw_str(canvas, start_pos, 22, text);
canvas_set_font(canvas, FontKeyboard);
for(uint8_t row = 0; row <= keyboard_row_count; row++) {
const uint8_t column_count = get_row_size(row);
const TextInputKey* keys = get_row(row);
for(size_t column = 0; column < column_count; column++) {
if(keys[column].text == ENTER_KEY) {
canvas_set_color(canvas, ColorBlack);
if(model->selected_row == row && model->selected_column == column) {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeySaveSelected_24x11);
} else {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeySave_24x11);
}
} else if(keys[column].text == BACKSPACE_KEY) {
canvas_set_color(canvas, ColorBlack);
if(model->selected_row == row && model->selected_column == column) {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeyBackspaceSelected_16x9);
} else {
canvas_draw_icon(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
&I_KeyBackspace_16x9);
}
} else {
if(model->selected_row == row && model->selected_column == column) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(
canvas,
keyboard_origin_x + keys[column].x - 1,
keyboard_origin_y + keys[column].y - 8,
7,
10);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_set_color(canvas, ColorBlack);
}
if(text_length == 0 && char_is_lowercase(keys[column].text)) {
canvas_draw_glyph(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
char_to_uppercase(keys[column].text));
} else {
canvas_draw_glyph(
canvas,
keyboard_origin_x + keys[column].x,
keyboard_origin_y + keys[column].y,
keys[column].text);
}
}
}
}
if(model->valadator_message_visible) {
canvas_set_font(canvas, FontSecondary);
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 8, 10, 110, 48);
canvas_set_color(canvas, ColorBlack);
canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42);
canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
elements_multiline_text(canvas, 62, 20, string_get_cstr(model->validator_text));
canvas_set_font(canvas, FontKeyboard);
}
}
static void text_input_handle_up(TextInput* text_input, TextInputModel* model) {
UNUSED(text_input);
if(model->selected_row > 0) {
model->selected_row--;
if(model->selected_column > get_row_size(model->selected_row) - 6) {
model->selected_column = model->selected_column + 1;
}
}
}
static void text_input_handle_down(TextInput* text_input, TextInputModel* model) {
UNUSED(text_input);
if(model->selected_row < keyboard_row_count - 1) {
model->selected_row++;
if(model->selected_column > get_row_size(model->selected_row) - 4) {
model->selected_column = model->selected_column - 1;
}
}
}
static void text_input_handle_left(TextInput* text_input, TextInputModel* model) {
UNUSED(text_input);
if(model->selected_column > 0) {
model->selected_column--;
} else {
model->selected_column = get_row_size(model->selected_row) - 1;
}
}
static void text_input_handle_right(TextInput* text_input, TextInputModel* model) {
UNUSED(text_input);
if(model->selected_column < get_row_size(model->selected_row) - 1) {
model->selected_column++;
} else {
model->selected_column = 0;
}
}
static void text_input_handle_ok(TextInput* text_input, TextInputModel* model, bool shift) {
char selected = get_selected_char(model);
uint8_t text_length = strlen(model->text_buffer);
if(shift) {
selected = char_to_uppercase(selected);
}
if(selected == ENTER_KEY) {
if(model->validator_callback &&
(!model->validator_callback(
model->text_buffer, model->validator_text, model->validator_callback_context))) {
model->valadator_message_visible = true;
furi_timer_start(text_input->timer, furi_kernel_get_tick_frequency() * 4);
} else if(model->callback != 0 && text_length > 0) {
model->callback(model->callback_context);
}
} else if(selected == BACKSPACE_KEY) {
text_input_backspace_cb(model);
} else if(text_length < (model->text_buffer_size - 1)) {
if(model->clear_default_text) {
text_length = 0;
}
if(text_length == 0 && char_is_lowercase(selected)) {
selected = char_to_uppercase(selected);
}
model->text_buffer[text_length] = selected;
model->text_buffer[text_length + 1] = 0;
}
model->clear_default_text = false;
}
static bool text_input_view_input_callback(InputEvent* event, void* context) {
TextInput* text_input = context;
furi_assert(text_input);
bool consumed = false;
// Acquire model
TextInputModel* model = view_get_model(text_input->view);
if((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) &&
model->valadator_message_visible) {
model->valadator_message_visible = false;
consumed = true;
} else if(event->type == InputTypeShort) {
consumed = true;
switch(event->key) {
case InputKeyUp:
text_input_handle_up(text_input, model);
break;
case InputKeyDown:
text_input_handle_down(text_input, model);
break;
case InputKeyLeft:
text_input_handle_left(text_input, model);
break;
case InputKeyRight:
text_input_handle_right(text_input, model);
break;
case InputKeyOk:
text_input_handle_ok(text_input, model, false);
break;
default:
consumed = false;
break;
}
} else if(event->type == InputTypeLong) {
consumed = true;
switch(event->key) {
case InputKeyUp:
text_input_handle_up(text_input, model);
break;
case InputKeyDown:
text_input_handle_down(text_input, model);
break;
case InputKeyLeft:
text_input_handle_left(text_input, model);
break;
case InputKeyRight:
text_input_handle_right(text_input, model);
break;
case InputKeyOk:
text_input_handle_ok(text_input, model, true);
break;
case InputKeyBack:
text_input_backspace_cb(model);
break;
default:
consumed = false;
break;
}
} else if(event->type == InputTypeRepeat) {
consumed = true;
switch(event->key) {
case InputKeyUp:
text_input_handle_up(text_input, model);
break;
case InputKeyDown:
text_input_handle_down(text_input, model);
break;
case InputKeyLeft:
text_input_handle_left(text_input, model);
break;
case InputKeyRight:
text_input_handle_right(text_input, model);
break;
case InputKeyBack:
text_input_backspace_cb(model);
break;
default:
consumed = false;
break;
}
}
// Commit model
view_commit_model(text_input->view, consumed);
return consumed;
}
void text_input_timer_callback(void* context) {
furi_assert(context);
TextInput* text_input = context;
with_view_model(
text_input->view, (TextInputModel * model) {
model->valadator_message_visible = false;
return true;
});
}
TextInput* text_input_alloc() {
TextInput* text_input = malloc(sizeof(TextInput));
text_input->view = view_alloc();
view_set_context(text_input->view, text_input);
view_allocate_model(text_input->view, ViewModelTypeLocking, sizeof(TextInputModel));
view_set_draw_callback(text_input->view, text_input_view_draw_callback);
view_set_input_callback(text_input->view, text_input_view_input_callback);
text_input->timer = furi_timer_alloc(text_input_timer_callback, FuriTimerTypeOnce, text_input);
with_view_model(
text_input->view, (TextInputModel * model) {
string_init(model->validator_text);
return false;
});
text_input_reset(text_input);
return text_input;
}
void text_input_free(TextInput* text_input) {
furi_assert(text_input);
with_view_model(
text_input->view, (TextInputModel * model) {
string_clear(model->validator_text);
return false;
});
// Send stop command
furi_timer_stop(text_input->timer);
// Release allocated memory
furi_timer_free(text_input->timer);
view_free(text_input->view);
free(text_input);
}
void text_input_reset(TextInput* text_input) {
furi_assert(text_input);
with_view_model(
text_input->view, (TextInputModel * model) {
model->text_buffer_size = 0;
model->header = "";
model->selected_row = 0;
model->selected_column = 0;
model->clear_default_text = false;
model->text_buffer = NULL;
model->text_buffer_size = 0;
model->callback = NULL;
model->callback_context = NULL;
model->validator_callback = NULL;
model->validator_callback_context = NULL;
string_reset(model->validator_text);
model->valadator_message_visible = false;
return true;
});
}
View* text_input_get_view(TextInput* text_input) {
furi_assert(text_input);
return text_input->view;
}
void text_input_set_result_callback(
TextInput* text_input,
TextInputCallback callback,
void* callback_context,
char* text_buffer,
size_t text_buffer_size,
bool clear_default_text) {
with_view_model(
text_input->view, (TextInputModel * model) {
model->callback = callback;
model->callback_context = callback_context;
model->text_buffer = text_buffer;
model->text_buffer_size = text_buffer_size;
model->clear_default_text = clear_default_text;
if(text_buffer && text_buffer[0] != '\0') {
// Set focus on Save
model->selected_row = 2;
model->selected_column = 8;
}
return true;
});
}
void text_input_set_validator(
TextInput* text_input,
TextInputValidatorCallback callback,
void* callback_context) {
with_view_model(
text_input->view, (TextInputModel * model) {
model->validator_callback = callback;
model->validator_callback_context = callback_context;
return true;
});
}
TextInputValidatorCallback text_input_get_validator_callback(TextInput* text_input) {
TextInputValidatorCallback validator_callback = NULL;
with_view_model(
text_input->view, (TextInputModel * model) {
validator_callback = model->validator_callback;
return false;
});
return validator_callback;
}
void* text_input_get_validator_callback_context(TextInput* text_input) {
void* validator_callback_context = NULL;
with_view_model(
text_input->view, (TextInputModel * model) {
validator_callback_context = model->validator_callback_context;
return false;
});
return validator_callback_context;
}
void text_input_set_header_text(TextInput* text_input, const char* text) {
with_view_model(
text_input->view, (TextInputModel * model) {
model->header = text;
return true;
});
}

View File

@@ -0,0 +1,87 @@
/**
* @file text_input.h
* GUI: TextInput keybord view module API
*/
#pragma once
#include <gui/view.h>
#include "validators.h"
#include <m-string.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Text input anonymous structure */
typedef struct TextInput TextInput;
typedef void (*TextInputCallback)(void* context);
typedef bool (*TextInputValidatorCallback)(const char* text, string_t error, void* context);
/** Allocate and initialize text input
*
* This text input is used to enter string
*
* @return TextInput instance
*/
TextInput* text_input_alloc();
/** Deinitialize and free text input
*
* @param text_input TextInput instance
*/
void text_input_free(TextInput* text_input);
/** Clean text input view Note: this function does not free memory
*
* @param text_input Text input instance
*/
void text_input_reset(TextInput* text_input);
/** Get text input view
*
* @param text_input TextInput instance
*
* @return View instance that can be used for embedding
*/
View* text_input_get_view(TextInput* text_input);
/** Set text input result callback
*
* @param text_input TextInput instance
* @param callback callback fn
* @param callback_context callback context
* @param text_buffer pointer to YOUR text buffer, that we going
* to modify
* @param text_buffer_size YOUR text buffer size in bytes. Max string
* length will be text_buffer_size-1.
* @param clear_default_text clear text from text_buffer on first OK
* event
*/
void text_input_set_result_callback(
TextInput* text_input,
TextInputCallback callback,
void* callback_context,
char* text_buffer,
size_t text_buffer_size,
bool clear_default_text);
void text_input_set_validator(
TextInput* text_input,
TextInputValidatorCallback callback,
void* callback_context);
TextInputValidatorCallback text_input_get_validator_callback(TextInput* text_input);
void* text_input_get_validator_callback_context(TextInput* text_input);
/** Set text input header text
*
* @param text_input TextInput instance
* @param text text to be shown
*/
void text_input_set_header_text(TextInput* text_input, const char* text);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,57 @@
#include <furi.h>
#include "validators.h"
#include <storage/storage.h>
struct ValidatorIsFile {
char* app_path_folder;
const char* app_extension;
char* current_name;
};
bool validator_is_file_callback(const char* text, string_t error, void* context) {
furi_assert(context);
ValidatorIsFile* instance = context;
if(instance->current_name != NULL) {
if(strcmp(instance->current_name, text) == 0) {
return true;
}
}
bool ret = true;
string_t path;
string_init_printf(path, "%s/%s%s", instance->app_path_folder, text, instance->app_extension);
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_common_stat(storage, string_get_cstr(path), NULL) == FSE_OK) {
ret = false;
string_printf(error, "This name\nexists!\nChoose\nanother one.");
} else {
ret = true;
}
string_clear(path);
furi_record_close(RECORD_STORAGE);
return ret;
}
ValidatorIsFile* validator_is_file_alloc_init(
const char* app_path_folder,
const char* app_extension,
const char* current_name) {
ValidatorIsFile* instance = malloc(sizeof(ValidatorIsFile));
instance->app_path_folder = strdup(app_path_folder);
instance->app_extension = app_extension;
if(current_name != NULL) {
instance->current_name = strdup(current_name);
}
return instance;
}
void validator_is_file_free(ValidatorIsFile* instance) {
furi_assert(instance);
free(instance->app_path_folder);
free(instance->current_name);
free(instance);
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <m-string.h>
#include <core/common_defines.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct ValidatorIsFile ValidatorIsFile;
ValidatorIsFile* validator_is_file_alloc_init(
const char* app_path_folder,
const char* app_extension,
const char* current_name);
void validator_is_file_free(ValidatorIsFile* instance);
bool validator_is_file_callback(const char* text, string_t error, void* context);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,388 @@
#include "variable_item_list.h"
#include "gui/canvas.h"
#include <m-array.h>
#include <furi.h>
#include <gui/elements.h>
#include <stdint.h>
struct VariableItem {
const char* label;
uint8_t current_value_index;
string_t current_value_text;
uint8_t values_count;
VariableItemChangeCallback change_callback;
void* context;
};
ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST);
struct VariableItemList {
View* view;
VariableItemListEnterCallback callback;
void* context;
};
typedef struct {
VariableItemArray_t items;
uint8_t position;
uint8_t window_position;
} VariableItemListModel;
static void variable_item_list_process_up(VariableItemList* variable_item_list);
static void variable_item_list_process_down(VariableItemList* variable_item_list);
static void variable_item_list_process_left(VariableItemList* variable_item_list);
static void variable_item_list_process_right(VariableItemList* variable_item_list);
static void variable_item_list_process_ok(VariableItemList* variable_item_list);
static void variable_item_list_draw_callback(Canvas* canvas, void* _model) {
VariableItemListModel* model = _model;
const uint8_t item_height = 16;
const uint8_t item_width = 123;
canvas_clear(canvas);
uint8_t position = 0;
VariableItemArray_it_t it;
canvas_set_font(canvas, FontSecondary);
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
uint8_t item_position = position - model->window_position;
uint8_t items_on_screen = 4;
uint8_t y_offset = 0;
if(item_position < items_on_screen) {
const VariableItem* item = VariableItemArray_cref(it);
uint8_t item_y = y_offset + (item_position * item_height);
uint8_t item_text_y = item_y + item_height - 4;
if(position == model->position) {
canvas_set_color(canvas, ColorBlack);
elements_slightly_rounded_box(canvas, 0, item_y + 1, item_width, item_height - 2);
canvas_set_color(canvas, ColorWhite);
} else {
canvas_set_color(canvas, ColorBlack);
}
canvas_draw_str(canvas, 6, item_text_y, item->label);
if(item->current_value_index > 0) {
canvas_draw_str(canvas, 73, item_text_y, "<");
}
canvas_draw_str_aligned(
canvas,
(115 + 73) / 2 + 1,
item_text_y,
AlignCenter,
AlignBottom,
string_get_cstr(item->current_value_text));
if(item->current_value_index < (item->values_count - 1)) {
canvas_draw_str(canvas, 115, item_text_y, ">");
}
}
position++;
}
elements_scrollbar(canvas, model->position, VariableItemArray_size(model->items));
}
void variable_item_list_set_selected_item(VariableItemList* variable_item_list, uint8_t index) {
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
uint8_t position = index;
if(position >= VariableItemArray_size(model->items)) {
position = 0;
}
model->position = position;
model->window_position = position;
if(model->window_position > 0) {
model->window_position -= 1;
}
if(VariableItemArray_size(model->items) <= 4) {
model->window_position = 0;
} else {
if(model->window_position >= (VariableItemArray_size(model->items) - 4)) {
model->window_position = (VariableItemArray_size(model->items) - 4);
}
}
return true;
});
}
uint8_t variable_item_list_get_selected_item_index(VariableItemList* variable_item_list) {
VariableItemListModel* model = view_get_model(variable_item_list->view);
uint8_t idx = model->position;
view_commit_model(variable_item_list->view, false);
return idx;
}
static bool variable_item_list_input_callback(InputEvent* event, void* context) {
VariableItemList* variable_item_list = context;
furi_assert(variable_item_list);
bool consumed = false;
if(event->type == InputTypeShort) {
switch(event->key) {
case InputKeyUp:
consumed = true;
variable_item_list_process_up(variable_item_list);
break;
case InputKeyDown:
consumed = true;
variable_item_list_process_down(variable_item_list);
break;
case InputKeyLeft:
consumed = true;
variable_item_list_process_left(variable_item_list);
break;
case InputKeyRight:
consumed = true;
variable_item_list_process_right(variable_item_list);
break;
case InputKeyOk:
variable_item_list_process_ok(variable_item_list);
break;
default:
break;
}
} else if(event->type == InputTypeRepeat) {
switch(event->key) {
case InputKeyUp:
consumed = true;
variable_item_list_process_up(variable_item_list);
break;
case InputKeyDown:
consumed = true;
variable_item_list_process_down(variable_item_list);
break;
case InputKeyLeft:
consumed = true;
variable_item_list_process_left(variable_item_list);
break;
case InputKeyRight:
consumed = true;
variable_item_list_process_right(variable_item_list);
break;
default:
break;
}
}
return consumed;
}
void variable_item_list_process_up(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
uint8_t items_on_screen = 4;
if(model->position > 0) {
model->position--;
if(((model->position - model->window_position) < 1) &&
model->window_position > 0) {
model->window_position--;
}
} else {
model->position = VariableItemArray_size(model->items) - 1;
if(model->position > (items_on_screen - 1)) {
model->window_position = model->position - (items_on_screen - 1);
}
}
return true;
});
}
void variable_item_list_process_down(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
uint8_t items_on_screen = 4;
if(model->position < (VariableItemArray_size(model->items) - 1)) {
model->position++;
if((model->position - model->window_position) > (items_on_screen - 2) &&
model->window_position <
(VariableItemArray_size(model->items) - items_on_screen)) {
model->window_position++;
}
} else {
model->position = 0;
model->window_position = 0;
}
return true;
});
}
VariableItem* variable_item_list_get_selected_item(VariableItemListModel* model) {
VariableItem* item = NULL;
VariableItemArray_it_t it;
uint8_t position = 0;
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
if(position == model->position) {
break;
}
position++;
}
item = VariableItemArray_ref(it);
furi_assert(item);
return item;
}
void variable_item_list_process_left(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
VariableItem* item = variable_item_list_get_selected_item(model);
if(item->current_value_index > 0) {
item->current_value_index--;
if(item->change_callback) {
item->change_callback(item);
}
}
return true;
});
}
void variable_item_list_process_right(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
VariableItem* item = variable_item_list_get_selected_item(model);
if(item->current_value_index < (item->values_count - 1)) {
item->current_value_index++;
if(item->change_callback) {
item->change_callback(item);
}
}
return true;
});
}
void variable_item_list_process_ok(VariableItemList* variable_item_list) {
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
if(variable_item_list->callback) {
variable_item_list->callback(variable_item_list->context, model->position);
}
return false;
});
}
VariableItemList* variable_item_list_alloc() {
VariableItemList* variable_item_list = malloc(sizeof(VariableItemList));
variable_item_list->view = view_alloc();
view_set_context(variable_item_list->view, variable_item_list);
view_allocate_model(
variable_item_list->view, ViewModelTypeLocking, sizeof(VariableItemListModel));
view_set_draw_callback(variable_item_list->view, variable_item_list_draw_callback);
view_set_input_callback(variable_item_list->view, variable_item_list_input_callback);
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
VariableItemArray_init(model->items);
model->position = 0;
model->window_position = 0;
return true;
});
return variable_item_list;
}
void variable_item_list_free(VariableItemList* variable_item_list) {
furi_assert(variable_item_list);
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
VariableItemArray_it_t it;
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
string_clear(VariableItemArray_ref(it)->current_value_text);
}
VariableItemArray_clear(model->items);
return false;
});
view_free(variable_item_list->view);
free(variable_item_list);
}
void variable_item_list_reset(VariableItemList* variable_item_list) {
furi_assert(variable_item_list);
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
VariableItemArray_it_t it;
for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
VariableItemArray_next(it)) {
string_clear(VariableItemArray_ref(it)->current_value_text);
}
VariableItemArray_reset(model->items);
return false;
});
}
View* variable_item_list_get_view(VariableItemList* variable_item_list) {
furi_assert(variable_item_list);
return variable_item_list->view;
}
VariableItem* variable_item_list_add(
VariableItemList* variable_item_list,
const char* label,
uint8_t values_count,
VariableItemChangeCallback change_callback,
void* context) {
VariableItem* item = NULL;
furi_assert(label);
furi_assert(variable_item_list);
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
item = VariableItemArray_push_new(model->items);
item->label = label;
item->values_count = values_count;
item->change_callback = change_callback;
item->context = context;
item->current_value_index = 0;
string_init(item->current_value_text);
return true;
});
return item;
}
void variable_item_list_set_enter_callback(
VariableItemList* variable_item_list,
VariableItemListEnterCallback callback,
void* context) {
furi_assert(callback);
with_view_model(
variable_item_list->view, (VariableItemListModel * model) {
UNUSED(model);
variable_item_list->callback = callback;
variable_item_list->context = context;
return false;
});
}
void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index) {
item->current_value_index = current_value_index;
}
void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text) {
string_set_str(item->current_value_text, current_value_text);
}
uint8_t variable_item_get_current_value_index(VariableItem* item) {
return item->current_value_index;
}
void* variable_item_get_context(VariableItem* item) {
return item->context;
}

View File

@@ -0,0 +1,109 @@
/**
* @file variable_item_list.h
* GUI: VariableItemList view module API
*/
#pragma once
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct VariableItemList VariableItemList;
typedef struct VariableItem VariableItem;
typedef void (*VariableItemChangeCallback)(VariableItem* item);
typedef void (*VariableItemListEnterCallback)(void* context, uint32_t index);
/** Allocate and initialize VariableItemList
*
* @return VariableItemList*
*/
VariableItemList* variable_item_list_alloc();
/** Deinitialize and free VariableItemList
*
* @param variable_item_list VariableItemList instance
*/
void variable_item_list_free(VariableItemList* variable_item_list);
/** Clear all elements from list
*
* @param variable_item_list VariableItemList instance
*/
void variable_item_list_reset(VariableItemList* variable_item_list);
/** Get VariableItemList View instance
*
* @param variable_item_list VariableItemList instance
*
* @return View instance
*/
View* variable_item_list_get_view(VariableItemList* variable_item_list);
/** Add item to VariableItemList
*
* @param variable_item_list VariableItemList instance
* @param label item name
* @param values_count item values count
* @param change_callback called on value change in gui
* @param context item context
*
* @return VariableItem* item instance
*/
VariableItem* variable_item_list_add(
VariableItemList* variable_item_list,
const char* label,
uint8_t values_count,
VariableItemChangeCallback change_callback,
void* context);
/** Set enter callback
*
* @param variable_item_list VariableItemList instance
* @param callback VariableItemListEnterCallback instance
* @param context pointer to context
*/
void variable_item_list_set_enter_callback(
VariableItemList* variable_item_list,
VariableItemListEnterCallback callback,
void* context);
void variable_item_list_set_selected_item(VariableItemList* variable_item_list, uint8_t index);
uint8_t variable_item_list_get_selected_item_index(VariableItemList* variable_item_list);
/** Set item current selected index
*
* @param item VariableItem* instance
* @param current_value_index The current value index
*/
void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index);
/** Set item current selected text
*
* @param item VariableItem* instance
* @param current_value_text The current value text
*/
void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text);
/** Get item current selected index
*
* @param item VariableItem* instance
*
* @return uint8_t current selected index
*/
uint8_t variable_item_get_current_value_index(VariableItem* item);
/** Get item context
*
* @param item VariableItem* instance
*
* @return void* item context
*/
void* variable_item_get_context(VariableItem* item);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,208 @@
#include <furi.h>
#include "widget.h"
#include <m-array.h>
#include "widget_elements/widget_element_i.h"
ARRAY_DEF(ElementArray, WidgetElement*, M_PTR_OPLIST);
struct Widget {
View* view;
void* context;
};
typedef struct {
ElementArray_t element;
} GuiWidgetModel;
static void gui_widget_view_draw_callback(Canvas* canvas, void* _model) {
GuiWidgetModel* model = _model;
canvas_clear(canvas);
// Draw all elements
ElementArray_it_t it;
ElementArray_it(it, model->element);
while(!ElementArray_end_p(it)) {
WidgetElement* element = *ElementArray_ref(it);
if(element->draw != NULL) {
element->draw(canvas, element);
}
ElementArray_next(it);
}
}
static bool gui_widget_view_input_callback(InputEvent* event, void* context) {
Widget* widget = context;
bool consumed = false;
// Call all Widget Elements input handlers
with_view_model(
widget->view, (GuiWidgetModel * model) {
ElementArray_it_t it;
ElementArray_it(it, model->element);
while(!ElementArray_end_p(it)) {
WidgetElement* element = *ElementArray_ref(it);
if(element->input != NULL) {
consumed |= element->input(event, element);
}
ElementArray_next(it);
}
return true;
});
return consumed;
}
Widget* widget_alloc() {
Widget* widget = malloc(sizeof(Widget));
widget->view = view_alloc();
view_set_context(widget->view, widget);
view_allocate_model(widget->view, ViewModelTypeLocking, sizeof(GuiWidgetModel));
view_set_draw_callback(widget->view, gui_widget_view_draw_callback);
view_set_input_callback(widget->view, gui_widget_view_input_callback);
with_view_model(
widget->view, (GuiWidgetModel * model) {
ElementArray_init(model->element);
return true;
});
return widget;
}
void widget_reset(Widget* widget) {
furi_assert(widget);
with_view_model(
widget->view, (GuiWidgetModel * model) {
ElementArray_it_t it;
ElementArray_it(it, model->element);
while(!ElementArray_end_p(it)) {
WidgetElement* element = *ElementArray_ref(it);
furi_assert(element->free);
element->free(element);
ElementArray_next(it);
}
ElementArray_reset(model->element);
return true;
});
}
void widget_free(Widget* widget) {
furi_assert(widget);
// Free all elements
widget_reset(widget);
// Free elements container
with_view_model(
widget->view, (GuiWidgetModel * model) {
ElementArray_clear(model->element);
return true;
});
view_free(widget->view);
free(widget);
}
View* widget_get_view(Widget* widget) {
furi_assert(widget);
return widget->view;
}
static void widget_add_element(Widget* widget, WidgetElement* element) {
furi_assert(widget);
furi_assert(element);
with_view_model(
widget->view, (GuiWidgetModel * model) {
element->parent = widget;
ElementArray_push_back(model->element, element);
return true;
});
}
void widget_add_string_multiline_element(
Widget* widget,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
Font font,
const char* text) {
furi_assert(widget);
WidgetElement* string_multiline_element =
widget_element_string_multiline_create(x, y, horizontal, vertical, font, text);
widget_add_element(widget, string_multiline_element);
}
void widget_add_string_element(
Widget* widget,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
Font font,
const char* text) {
furi_assert(widget);
WidgetElement* string_element =
widget_element_string_create(x, y, horizontal, vertical, font, text);
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,
bool strip_to_dots) {
furi_assert(widget);
WidgetElement* text_box_element = widget_element_text_box_create(
x, y, width, height, horizontal, vertical, text, strip_to_dots);
widget_add_element(widget, text_box_element);
}
void widget_add_text_scroll_element(
Widget* widget,
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
const char* text) {
furi_assert(widget);
WidgetElement* text_scroll_element =
widget_element_text_scroll_create(x, y, width, height, text);
widget_add_element(widget, text_scroll_element);
}
void widget_add_button_element(
Widget* widget,
GuiButtonType button_type,
const char* text,
ButtonCallback callback,
void* context) {
furi_assert(widget);
WidgetElement* button_element =
widget_element_button_create(button_type, text, callback, context);
widget_add_element(widget, button_element);
}
void widget_add_icon_element(Widget* widget, uint8_t x, uint8_t y, const Icon* icon) {
furi_assert(widget);
furi_assert(icon);
WidgetElement* icon_element = widget_element_icon_create(x, y, icon);
widget_add_element(widget, icon_element);
}
void widget_add_frame_element(
Widget* widget,
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
uint8_t radius) {
furi_assert(widget);
WidgetElement* frame_element = widget_element_frame_create(x, y, width, height, radius);
widget_add_element(widget, frame_element);
}

View File

@@ -0,0 +1,172 @@
/**
* @file widget.h
* GUI: Widget view module API
*/
#pragma once
#include <gui/view.h>
#include "widget_elements/widget_element.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Widget Widget;
typedef struct WidgetElement WidgetElement;
/** Allocate Widget that holds Widget Elements
*
* @return Widget instance
*/
Widget* widget_alloc();
/** Free Widget
* @note this function free allocated Widget Elements
*
* @param widget Widget instance
*/
void widget_free(Widget* widget);
/** Reset Widget
*
* @param widget Widget instance
*/
void widget_reset(Widget* widget);
/** Get Widget view
*
* @param widget Widget instance
*
* @return View instance
*/
View* widget_get_view(Widget* widget);
/** Add Multi String Element
*
* @param widget Widget instance
* @param x x coordinate
* @param y y coordinate
* @param horizontal Align instance
* @param vertical Align instance
* @param font Font instance
* @param[in] text The text
*/
void widget_add_string_multiline_element(
Widget* widget,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
Font font,
const char* text);
/** Add String Element
*
* @param widget Widget instance
* @param x x coordinate
* @param y y coordinate
* @param horizontal Align instance
* @param vertical Align instance
* @param font Font instance
* @param[in] text The text
*/
void widget_add_string_element(
Widget* widget,
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
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
* @param strip_to_dots Strip text to ... if does not fit to width
*/
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,
bool strip_to_dots);
/** Add Text Scroll 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[in] text Formatted text. Default format: align left, Secondary font.
* The following formats are available:
* "\e#Bold text" - sets bold font before until next '\n' symbol
* "\ecCenter-aligned text" - sets center horizontal align until the next '\n' symbol
* "\erRight-aligned text" - sets right horizontal align until the next '\n' symbol
*/
void widget_add_text_scroll_element(
Widget* widget,
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
const char* text);
/** Add Button Element
*
* @param widget Widget instance
* @param button_type GuiButtonType instance
* @param text text on allocated button
* @param callback ButtonCallback instance
* @param context pointer to context
*/
void widget_add_button_element(
Widget* widget,
GuiButtonType button_type,
const char* text,
ButtonCallback callback,
void* context);
/** Add Icon Element
*
* @param widget Widget instance
* @param x top left x coordinate
* @param y top left y coordinate
* @param icon Icon instance
*/
void widget_add_icon_element(Widget* widget, uint8_t x, uint8_t y, const Icon* icon);
/** Add Frame Element
*
* @param widget Widget instance
* @param x top left x coordinate
* @param y top left y coordinate
* @param width frame width
* @param height frame height
* @param radius frame radius
*/
void widget_add_frame_element(
Widget* widget,
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
uint8_t radius);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,22 @@
/**
* @file widget_element_i.h
* GUI: internal Widget Element API
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
GuiButtonTypeLeft,
GuiButtonTypeCenter,
GuiButtonTypeRight,
} GuiButtonType;
typedef void (*ButtonCallback)(GuiButtonType result, InputType type, void* context);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,79 @@
#include "widget_element_i.h"
#include <gui/elements.h>
#include <m-string.h>
typedef struct {
GuiButtonType button_type;
string_t text;
ButtonCallback callback;
void* context;
} GuiButtonModel;
static void gui_button_draw(Canvas* canvas, WidgetElement* element) {
furi_assert(canvas);
furi_assert(element);
GuiButtonModel* model = element->model;
canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontSecondary);
if(model->button_type == GuiButtonTypeLeft) {
elements_button_left(canvas, string_get_cstr(model->text));
} else if(model->button_type == GuiButtonTypeRight) {
elements_button_right(canvas, string_get_cstr(model->text));
} else if(model->button_type == GuiButtonTypeCenter) {
elements_button_center(canvas, string_get_cstr(model->text));
}
}
static bool gui_button_input(InputEvent* event, WidgetElement* element) {
GuiButtonModel* model = element->model;
bool consumed = false;
if(model->callback == NULL) return consumed;
if((model->button_type == GuiButtonTypeLeft) && (event->key == InputKeyLeft)) {
model->callback(model->button_type, event->type, model->context);
consumed = true;
} else if((model->button_type == GuiButtonTypeRight) && (event->key == InputKeyRight)) {
model->callback(model->button_type, event->type, model->context);
consumed = true;
} else if((model->button_type == GuiButtonTypeCenter) && (event->key == InputKeyOk)) {
model->callback(model->button_type, event->type, model->context);
consumed = true;
}
return consumed;
}
static void gui_button_free(WidgetElement* gui_button) {
furi_assert(gui_button);
GuiButtonModel* model = gui_button->model;
string_clear(model->text);
free(gui_button->model);
free(gui_button);
}
WidgetElement* widget_element_button_create(
GuiButtonType button_type,
const char* text,
ButtonCallback callback,
void* context) {
// Allocate and init model
GuiButtonModel* model = malloc(sizeof(GuiButtonModel));
model->button_type = button_type;
model->callback = callback;
model->context = context;
string_init_set_str(model->text, text);
// Allocate and init Element
WidgetElement* gui_button = malloc(sizeof(WidgetElement));
gui_button->parent = NULL;
gui_button->input = gui_button_input;
gui_button->draw = gui_button_draw;
gui_button->free = gui_button_free;
gui_button->model = model;
return gui_button;
}

View File

@@ -0,0 +1,48 @@
#include "widget_element_i.h"
typedef struct {
uint8_t x;
uint8_t y;
uint8_t width;
uint8_t height;
uint8_t radius;
} GuiFrameModel;
static void gui_frame_draw(Canvas* canvas, WidgetElement* element) {
furi_assert(canvas);
furi_assert(element);
GuiFrameModel* model = element->model;
canvas_draw_rframe(canvas, model->x, model->y, model->width, model->height, model->radius);
}
static void gui_frame_free(WidgetElement* gui_frame) {
furi_assert(gui_frame);
free(gui_frame->model);
free(gui_frame);
}
WidgetElement* widget_element_frame_create(
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
uint8_t radius) {
// Allocate and init model
GuiFrameModel* model = malloc(sizeof(GuiFrameModel));
model->x = x;
model->y = y;
model->width = width;
model->height = height;
model->radius = radius;
// Allocate and init Element
WidgetElement* gui_frame = malloc(sizeof(WidgetElement));
gui_frame->parent = NULL;
gui_frame->input = NULL;
gui_frame->draw = gui_frame_draw;
gui_frame->free = gui_frame_free;
gui_frame->model = model;
return gui_frame;
}

View File

@@ -0,0 +1,91 @@
/**
* @file widget_element_i.h
* GUI: internal Widget Element API
*/
#pragma once
#include <furi.h>
#include <gui/view.h>
#include <input/input.h>
#include "widget_element.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct WidgetElement WidgetElement;
typedef struct Widget Widget;
struct WidgetElement {
// generic draw and input callbacks
void (*draw)(Canvas* canvas, WidgetElement* element);
bool (*input)(InputEvent* event, WidgetElement* element);
// free callback
void (*free)(WidgetElement* element);
// generic model holder
void* model;
FuriMutex* model_mutex;
// pointer to widget that hold our element
Widget* parent;
};
/** Create multi string element */
WidgetElement* widget_element_string_multiline_create(
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
Font font,
const char* text);
/** Create string element */
WidgetElement* widget_element_string_create(
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
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,
bool strip_to_dots);
/** Create button element */
WidgetElement* widget_element_button_create(
GuiButtonType button_type,
const char* text,
ButtonCallback callback,
void* context);
/** Create icon element */
WidgetElement* widget_element_icon_create(uint8_t x, uint8_t y, const Icon* icon);
/** Create frame element */
WidgetElement* widget_element_frame_create(
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
uint8_t radius);
WidgetElement* widget_element_text_scroll_create(
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
const char* text);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,44 @@
#include "widget_element_i.h"
typedef struct {
uint8_t x;
uint8_t y;
const Icon* icon;
} GuiIconModel;
static void gui_icon_draw(Canvas* canvas, WidgetElement* element) {
furi_assert(canvas);
furi_assert(element);
GuiIconModel* model = element->model;
if(model->icon) {
canvas_draw_icon(canvas, model->x, model->y, model->icon);
}
}
static void gui_icon_free(WidgetElement* gui_icon) {
furi_assert(gui_icon);
free(gui_icon->model);
free(gui_icon);
}
WidgetElement* widget_element_icon_create(uint8_t x, uint8_t y, const Icon* icon) {
furi_assert(icon);
// Allocate and init model
GuiIconModel* model = malloc(sizeof(GuiIconModel));
model->x = x;
model->y = y;
model->icon = icon;
// Allocate and init Element
WidgetElement* gui_icon = malloc(sizeof(WidgetElement));
gui_icon->parent = NULL;
gui_icon->input = NULL;
gui_icon->draw = gui_icon_draw;
gui_icon->free = gui_icon_free;
gui_icon->model = model;
return gui_icon;
}

View File

@@ -0,0 +1,66 @@
#include "widget_element_i.h"
#include <m-string.h>
typedef struct {
uint8_t x;
uint8_t y;
Align horizontal;
Align vertical;
Font font;
string_t text;
} GuiStringModel;
static void gui_string_draw(Canvas* canvas, WidgetElement* element) {
furi_assert(canvas);
furi_assert(element);
GuiStringModel* model = element->model;
if(string_size(model->text)) {
canvas_set_font(canvas, model->font);
canvas_draw_str_aligned(
canvas,
model->x,
model->y,
model->horizontal,
model->vertical,
string_get_cstr(model->text));
}
}
static void gui_string_free(WidgetElement* gui_string) {
furi_assert(gui_string);
GuiStringModel* model = gui_string->model;
string_clear(model->text);
free(gui_string->model);
free(gui_string);
}
WidgetElement* widget_element_string_create(
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
Font font,
const char* text) {
furi_assert(text);
// Allocate and init model
GuiStringModel* model = malloc(sizeof(GuiStringModel));
model->x = x;
model->y = y;
model->horizontal = horizontal;
model->vertical = vertical;
model->font = font;
string_init_set_str(model->text, text);
// Allocate and init Element
WidgetElement* gui_string = malloc(sizeof(WidgetElement));
gui_string->parent = NULL;
gui_string->input = NULL;
gui_string->draw = gui_string_draw;
gui_string->free = gui_string_free;
gui_string->model = model;
return gui_string;
}

View File

@@ -0,0 +1,67 @@
#include "widget_element_i.h"
#include <m-string.h>
#include <gui/elements.h>
typedef struct {
uint8_t x;
uint8_t y;
Align horizontal;
Align vertical;
Font font;
string_t text;
} GuiStringMultiLineModel;
static void gui_string_multiline_draw(Canvas* canvas, WidgetElement* element) {
furi_assert(canvas);
furi_assert(element);
GuiStringMultiLineModel* model = element->model;
if(string_size(model->text)) {
canvas_set_font(canvas, model->font);
elements_multiline_text_aligned(
canvas,
model->x,
model->y,
model->horizontal,
model->vertical,
string_get_cstr(model->text));
}
}
static void gui_string_multiline_free(WidgetElement* gui_string) {
furi_assert(gui_string);
GuiStringMultiLineModel* model = gui_string->model;
string_clear(model->text);
free(gui_string->model);
free(gui_string);
}
WidgetElement* widget_element_string_multiline_create(
uint8_t x,
uint8_t y,
Align horizontal,
Align vertical,
Font font,
const char* text) {
furi_assert(text);
// Allocate and init model
GuiStringMultiLineModel* model = malloc(sizeof(GuiStringMultiLineModel));
model->x = x;
model->y = y;
model->horizontal = horizontal;
model->vertical = vertical;
model->font = font;
string_init_set_str(model->text, text);
// Allocate and init Element
WidgetElement* gui_string = malloc(sizeof(WidgetElement));
gui_string->parent = NULL;
gui_string->input = NULL;
gui_string->draw = gui_string_multiline_draw;
gui_string->free = gui_string_multiline_free;
gui_string->model = model;
return gui_string;
}

View File

@@ -0,0 +1,75 @@
#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;
bool strip_to_dots;
} 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),
model->strip_to_dots);
}
}
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,
bool strip_to_dots) {
furi_assert(text);
// Allocate and init model
GuiTextBoxModel* model = malloc(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);
model->strip_to_dots = strip_to_dots;
// Allocate and init Element
WidgetElement* gui_string = malloc(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;
}

View File

@@ -0,0 +1,245 @@
#include "widget_element_i.h"
#include <m-string.h>
#include <gui/elements.h>
#include <m-array.h>
#define WIDGET_ELEMENT_TEXT_SCROLL_BAR_OFFSET (4)
typedef struct {
Font font;
Align horizontal;
string_t text;
} TextScrollLineArray;
ARRAY_DEF(TextScrollLineArray, TextScrollLineArray, M_POD_OPLIST)
typedef struct {
TextScrollLineArray_t line_array;
uint8_t x;
uint8_t y;
uint8_t width;
uint8_t height;
string_t text;
uint8_t scroll_pos_total;
uint8_t scroll_pos_current;
bool text_formatted;
} WidgetElementTextScrollModel;
static bool
widget_element_text_scroll_process_ctrl_symbols(TextScrollLineArray* line, string_t text) {
bool processed = false;
do {
if(string_get_char(text, 0) != '\e') break;
char ctrl_symbol = string_get_char(text, 1);
if(ctrl_symbol == 'c') {
line->horizontal = AlignCenter;
} else if(ctrl_symbol == 'r') {
line->horizontal = AlignRight;
} else if(ctrl_symbol == '#') {
line->font = FontPrimary;
}
string_right(text, 2);
processed = true;
} while(false);
return processed;
}
void widget_element_text_scroll_add_line(WidgetElement* element, TextScrollLineArray* line) {
WidgetElementTextScrollModel* model = element->model;
TextScrollLineArray new_line;
new_line.font = line->font;
new_line.horizontal = line->horizontal;
string_init_set(new_line.text, line->text);
TextScrollLineArray_push_back(model->line_array, new_line);
}
static void widget_element_text_scroll_fill_lines(Canvas* canvas, WidgetElement* element) {
WidgetElementTextScrollModel* model = element->model;
TextScrollLineArray line_tmp;
bool all_text_processed = false;
string_init(line_tmp.text);
bool reached_new_line = true;
uint16_t total_height = 0;
while(!all_text_processed) {
if(reached_new_line) {
// Set default line properties
line_tmp.font = FontSecondary;
line_tmp.horizontal = AlignLeft;
string_reset(line_tmp.text);
// Process control symbols
while(widget_element_text_scroll_process_ctrl_symbols(&line_tmp, model->text))
;
}
// Set canvas font
canvas_set_font(canvas, line_tmp.font);
CanvasFontParameters* params = canvas_get_font_params(canvas, line_tmp.font);
total_height += params->height;
if(total_height > model->height) {
model->scroll_pos_total++;
}
uint8_t line_width = 0;
uint16_t char_i = 0;
while(true) {
char next_char = string_get_char(model->text, char_i++);
if(next_char == '\0') {
string_push_back(line_tmp.text, '\0');
widget_element_text_scroll_add_line(element, &line_tmp);
total_height += params->leading_default - params->height;
all_text_processed = true;
break;
} else if(next_char == '\n') {
string_push_back(line_tmp.text, '\0');
widget_element_text_scroll_add_line(element, &line_tmp);
string_right(model->text, char_i);
total_height += params->leading_default - params->height;
reached_new_line = true;
break;
} else {
line_width += canvas_glyph_width(canvas, next_char);
if(line_width > model->width) {
string_push_back(line_tmp.text, '\0');
widget_element_text_scroll_add_line(element, &line_tmp);
string_right(model->text, char_i - 1);
string_reset(line_tmp.text);
total_height += params->leading_default - params->height;
reached_new_line = false;
break;
} else {
string_push_back(line_tmp.text, next_char);
}
}
}
}
string_clear(line_tmp.text);
}
static void widget_element_text_scroll_draw(Canvas* canvas, WidgetElement* element) {
furi_assert(canvas);
furi_assert(element);
furi_mutex_acquire(element->model_mutex, FuriWaitForever);
WidgetElementTextScrollModel* model = element->model;
if(!model->text_formatted) {
widget_element_text_scroll_fill_lines(canvas, element);
model->text_formatted = true;
}
uint8_t y = model->y;
uint8_t x = model->x;
uint16_t curr_line = 0;
if(TextScrollLineArray_size(model->line_array)) {
TextScrollLineArray_it_t it;
for(TextScrollLineArray_it(it, model->line_array); !TextScrollLineArray_end_p(it);
TextScrollLineArray_next(it), curr_line++) {
if(curr_line < model->scroll_pos_current) continue;
TextScrollLineArray* line = TextScrollLineArray_ref(it);
CanvasFontParameters* params = canvas_get_font_params(canvas, line->font);
if(y + params->descender > model->y + model->height) break;
canvas_set_font(canvas, line->font);
if(line->horizontal == AlignLeft) {
x = model->x;
} else if(line->horizontal == AlignCenter) {
x = (model->x + model->width) / 2;
} else if(line->horizontal == AlignRight) {
x = model->x + model->width;
}
canvas_draw_str_aligned(
canvas, x, y, line->horizontal, AlignTop, string_get_cstr(line->text));
y += params->leading_default;
}
// Draw scroll bar
if(model->scroll_pos_total > 1) {
elements_scrollbar_pos(
canvas,
model->x + model->width + WIDGET_ELEMENT_TEXT_SCROLL_BAR_OFFSET,
model->y,
model->height,
model->scroll_pos_current,
model->scroll_pos_total);
}
}
furi_mutex_release(element->model_mutex);
}
static bool widget_element_text_scroll_input(InputEvent* event, WidgetElement* element) {
furi_assert(event);
furi_assert(element);
furi_mutex_acquire(element->model_mutex, FuriWaitForever);
WidgetElementTextScrollModel* model = element->model;
bool consumed = false;
if((event->type == InputTypeShort) || (event->type == InputTypeRepeat)) {
if(event->key == InputKeyUp) {
if(model->scroll_pos_current > 0) {
model->scroll_pos_current--;
}
consumed = true;
} else if(event->key == InputKeyDown) {
if((model->scroll_pos_total > 1) &&
(model->scroll_pos_current < model->scroll_pos_total - 1)) {
model->scroll_pos_current++;
}
consumed = true;
}
}
furi_mutex_release(element->model_mutex);
return consumed;
}
static void widget_element_text_scroll_free(WidgetElement* text_scroll) {
furi_assert(text_scroll);
WidgetElementTextScrollModel* model = text_scroll->model;
TextScrollLineArray_it_t it;
for(TextScrollLineArray_it(it, model->line_array); !TextScrollLineArray_end_p(it);
TextScrollLineArray_next(it)) {
TextScrollLineArray* line = TextScrollLineArray_ref(it);
string_clear(line->text);
}
TextScrollLineArray_clear(model->line_array);
string_clear(model->text);
free(text_scroll->model);
furi_mutex_free(text_scroll->model_mutex);
free(text_scroll);
}
WidgetElement* widget_element_text_scroll_create(
uint8_t x,
uint8_t y,
uint8_t width,
uint8_t height,
const char* text) {
furi_assert(text);
// Allocate and init model
WidgetElementTextScrollModel* model = malloc(sizeof(WidgetElementTextScrollModel));
model->x = x;
model->y = y;
model->width = width - WIDGET_ELEMENT_TEXT_SCROLL_BAR_OFFSET;
model->height = height;
model->scroll_pos_current = 0;
model->scroll_pos_total = 1;
TextScrollLineArray_init(model->line_array);
string_init_set_str(model->text, text);
WidgetElement* text_scroll = malloc(sizeof(WidgetElement));
text_scroll->parent = NULL;
text_scroll->draw = widget_element_text_scroll_draw;
text_scroll->input = widget_element_text_scroll_input;
text_scroll->free = widget_element_text_scroll_free;
text_scroll->model = model;
text_scroll->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
return text_scroll;
}