mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-12 09:58:37 -07:00
[FL-3816] Text Box rework (#3642)
* text box: rework text box to use less memory * text box: optimize lines iteration * text box: fix back scrolling * text box: add end focus support * text box: allocate index buffer on stack instead of heap * text box: fix index decrement * apps: add text box debug app * debug: rework text box view app with view stack * debug app: rename text_box_test app to text_box_element_test * text box: rework text box module * debug: update text box view test * text box: fix end focus initial offset * debug: fix memory leak in text box view test app Co-authored-by: あく <alleteam@gmail.com> Co-authored-by: hedger <hedger@users.noreply.github.com>
This commit is contained in:
@@ -4,8 +4,13 @@
|
||||
#include <furi.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define TEXT_BOX_MAX_SYMBOL_WIDTH (10)
|
||||
#define TEXT_BOX_LINE_WIDTH (120)
|
||||
#define TEXT_BOX_TEXT_WIDTH (120)
|
||||
#define TEXT_BOX_TEXT_HEIGHT (56)
|
||||
#define TEXT_BOX_MAX_LINES_PER_SCREEN (10)
|
||||
|
||||
#define TEXT_BOX_LINES_SCROLL_SPEED_MEDIUM (3)
|
||||
#define TEXT_BOX_LINES_SCROLL_SPEED_FAST (5)
|
||||
#define TEXT_BOX_LINES_SCROLL_SPEED_SATURATION (9)
|
||||
|
||||
struct TextBox {
|
||||
View* view;
|
||||
@@ -14,13 +19,19 @@ struct TextBox {
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char* text;
|
||||
char* text_pos;
|
||||
FuriString* text_formatted;
|
||||
int32_t scroll_pos;
|
||||
int32_t scroll_num;
|
||||
TextBoxFont font;
|
||||
TextBoxFocus focus;
|
||||
const char* text;
|
||||
|
||||
int32_t scroll_pos;
|
||||
int32_t scroll_num;
|
||||
int32_t lines_on_screen;
|
||||
|
||||
int32_t line_offset;
|
||||
int32_t text_offset;
|
||||
FuriString* text_on_screen;
|
||||
FuriString* text_line;
|
||||
|
||||
bool formatted;
|
||||
} TextBoxModel;
|
||||
|
||||
@@ -29,20 +40,11 @@ static void text_box_process_down(TextBox* text_box, uint8_t lines) {
|
||||
text_box->view,
|
||||
TextBoxModel * model,
|
||||
{
|
||||
if(model->scroll_pos < model->scroll_num - lines) {
|
||||
if(model->scroll_pos + lines < model->scroll_num) {
|
||||
model->scroll_pos += lines;
|
||||
for(uint8_t i = 0; i < lines; i++) {
|
||||
// Search next line start
|
||||
while(*model->text_pos++ != '\n')
|
||||
;
|
||||
}
|
||||
} else if(lines > 1) {
|
||||
lines = model->scroll_num - model->scroll_pos - 1;
|
||||
model->scroll_pos = model->scroll_num - 1;
|
||||
for(uint8_t i = 0; i < lines; i++) {
|
||||
// Search next line start
|
||||
while(*model->text_pos++ != '\n')
|
||||
;
|
||||
} else {
|
||||
if(model->scroll_num > 0) {
|
||||
model->scroll_pos = model->scroll_num - 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -54,69 +56,196 @@ static void text_box_process_up(TextBox* text_box, uint8_t lines) {
|
||||
text_box->view,
|
||||
TextBoxModel * model,
|
||||
{
|
||||
if(model->scroll_pos > lines - 1) {
|
||||
if(model->scroll_pos - lines > 0) {
|
||||
model->scroll_pos -= lines;
|
||||
for(uint8_t i = 0; i < lines; i++) {
|
||||
// Reach last symbol of previous line
|
||||
model->text_pos--;
|
||||
// Search previous line start
|
||||
while((model->text_pos != model->text) && (*(--model->text_pos) != '\n'))
|
||||
;
|
||||
if(*model->text_pos == '\n') {
|
||||
model->text_pos++;
|
||||
}
|
||||
}
|
||||
} else if(lines > 1) {
|
||||
lines = model->scroll_pos;
|
||||
} else {
|
||||
model->scroll_pos = 0;
|
||||
model->text_pos = (char*)model->text;
|
||||
}
|
||||
},
|
||||
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;
|
||||
static bool text_box_view_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
|
||||
while(str[i] != '\0') {
|
||||
char symb = str[i++];
|
||||
if(symb != '\n') {
|
||||
TextBox* text_box = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
|
||||
int32_t scroll_speed = 1;
|
||||
if(text_box->button_held_for_ticks > TEXT_BOX_LINES_SCROLL_SPEED_FAST) {
|
||||
if(text_box->button_held_for_ticks % 2) {
|
||||
scroll_speed = 0;
|
||||
} else {
|
||||
scroll_speed =
|
||||
(text_box->button_held_for_ticks > TEXT_BOX_LINES_SCROLL_SPEED_SATURATION) ?
|
||||
TEXT_BOX_LINES_SCROLL_SPEED_FAST :
|
||||
TEXT_BOX_LINES_SCROLL_SPEED_MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
if(event->key == InputKeyDown) {
|
||||
text_box_process_down(text_box, scroll_speed);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyUp) {
|
||||
text_box_process_up(text_box, scroll_speed);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
text_box->button_held_for_ticks++;
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
text_box->button_held_for_ticks = 0;
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
static bool text_box_end_of_text_reached(TextBoxModel* model) {
|
||||
return model->text[model->text_offset] == '\0';
|
||||
}
|
||||
|
||||
static bool text_box_start_of_text_reached(TextBoxModel* model) {
|
||||
return model->text_offset == 0;
|
||||
}
|
||||
|
||||
static void text_box_seek_next_line(Canvas* canvas, TextBoxModel* model) {
|
||||
size_t line_width = 0;
|
||||
|
||||
while(!text_box_end_of_text_reached(model)) {
|
||||
char symb = model->text[model->text_offset];
|
||||
if(symb == '\n') {
|
||||
model->text_offset++;
|
||||
break;
|
||||
} else {
|
||||
size_t glyph_width = canvas_glyph_width(canvas, symb);
|
||||
if(line_width + glyph_width > TEXT_BOX_LINE_WIDTH) {
|
||||
line_num++;
|
||||
line_width = 0;
|
||||
furi_string_push_back(model->text_formatted, '\n');
|
||||
if(line_width + glyph_width > TEXT_BOX_TEXT_WIDTH) {
|
||||
break;
|
||||
}
|
||||
line_width += glyph_width;
|
||||
} else {
|
||||
line_num++;
|
||||
line_width = 0;
|
||||
model->text_offset++;
|
||||
}
|
||||
furi_string_push_back(model->text_formatted, symb);
|
||||
}
|
||||
line_num++;
|
||||
model->text = furi_string_get_cstr(model->text_formatted);
|
||||
model->text_pos = (char*)model->text;
|
||||
size_t lines_on_screen = 56 / canvas_current_font_height(canvas);
|
||||
if(model->focus == TextBoxFocusEnd && line_num > lines_on_screen) {
|
||||
// Set text position to 5th line from the end
|
||||
const char* end = model->text + furi_string_size(model->text_formatted);
|
||||
for(size_t i = 0; i < line_num - lines_on_screen; i++) {
|
||||
while(model->text_pos < end) {
|
||||
if(*model->text_pos++ == '\n') break;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_box_seek_end_of_prev_line(TextBoxModel* model) {
|
||||
do {
|
||||
if(text_box_start_of_text_reached(model)) break;
|
||||
model->text_offset--;
|
||||
if(text_box_start_of_text_reached(model)) break;
|
||||
if(model->text[model->text_offset] == '\n') {
|
||||
model->text_offset--;
|
||||
}
|
||||
} while(false);
|
||||
}
|
||||
|
||||
static void text_box_seek_prev_paragraph(TextBoxModel* model) {
|
||||
while(!text_box_start_of_text_reached(model)) {
|
||||
if(model->text[model->text_offset] == '\n') {
|
||||
model->text_offset++;
|
||||
break;
|
||||
}
|
||||
model->text_offset--;
|
||||
}
|
||||
}
|
||||
|
||||
static void text_box_seek_prev_line(Canvas* canvas, TextBoxModel* model) {
|
||||
int32_t start_text_offset = model->text_offset;
|
||||
|
||||
text_box_seek_end_of_prev_line(model);
|
||||
text_box_seek_prev_paragraph(model);
|
||||
|
||||
int32_t current_text_offset = model->text_offset;
|
||||
while(true) {
|
||||
text_box_seek_next_line(canvas, model);
|
||||
if(model->text_offset == start_text_offset) {
|
||||
break;
|
||||
}
|
||||
current_text_offset = model->text_offset;
|
||||
}
|
||||
model->text_offset = current_text_offset;
|
||||
}
|
||||
|
||||
static void text_box_move_line_offset(Canvas* canvas, TextBoxModel* model, int32_t line_offset) {
|
||||
if(line_offset >= 0) {
|
||||
for(int32_t i = 0; i < line_offset; i++) {
|
||||
text_box_seek_next_line(canvas, model);
|
||||
}
|
||||
model->scroll_num = line_num - (lines_on_screen - 1);
|
||||
model->scroll_pos = line_num - lines_on_screen;
|
||||
} else {
|
||||
model->scroll_num = MAX(line_num - (lines_on_screen - 1), 0u);
|
||||
model->scroll_pos = 0;
|
||||
for(int32_t i = 0; i < (-line_offset); i++) {
|
||||
text_box_seek_prev_line(canvas, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void text_box_update_screen_text(Canvas* canvas, TextBoxModel* model) {
|
||||
furi_string_reset(model->text_on_screen);
|
||||
furi_string_reset(model->text_line);
|
||||
|
||||
int32_t start_text_offset = model->text_offset;
|
||||
|
||||
for(int32_t i = 0; i < model->lines_on_screen; i++) {
|
||||
int32_t current_line_text_offset = model->text_offset;
|
||||
text_box_seek_next_line(canvas, model);
|
||||
int32_t next_line_text_offset = model->text_offset;
|
||||
furi_string_set_strn(
|
||||
model->text_line,
|
||||
&model->text[current_line_text_offset],
|
||||
next_line_text_offset - current_line_text_offset);
|
||||
size_t str_len = furi_string_size(model->text_line);
|
||||
if(furi_string_get_char(model->text_line, str_len - 1) != '\n') {
|
||||
furi_string_push_back(model->text_line, '\n');
|
||||
}
|
||||
furi_string_cat(model->text_on_screen, model->text_line);
|
||||
|
||||
if(text_box_end_of_text_reached(model)) break;
|
||||
current_line_text_offset = next_line_text_offset;
|
||||
}
|
||||
|
||||
model->text_offset = start_text_offset;
|
||||
}
|
||||
|
||||
static void text_box_update_text_on_screen(Canvas* canvas, TextBoxModel* model) {
|
||||
int32_t line_offset = model->scroll_pos - model->line_offset;
|
||||
text_box_move_line_offset(canvas, model, line_offset);
|
||||
text_box_update_screen_text(canvas, model);
|
||||
model->line_offset = model->scroll_pos;
|
||||
}
|
||||
|
||||
static void text_box_prepare_model(Canvas* canvas, TextBoxModel* model) {
|
||||
int32_t lines_num = 0;
|
||||
model->text_offset = 0;
|
||||
model->scroll_num = 0;
|
||||
model->scroll_pos = 0;
|
||||
model->line_offset = 0;
|
||||
model->lines_on_screen = TEXT_BOX_TEXT_HEIGHT / canvas_current_font_height(canvas);
|
||||
|
||||
// Cache text offset to quick final text offset update if TextBoxFocusEnd is set
|
||||
int32_t window_offset[TEXT_BOX_MAX_LINES_PER_SCREEN] = {};
|
||||
do {
|
||||
window_offset[lines_num % model->lines_on_screen] = model->text_offset;
|
||||
text_box_seek_next_line(canvas, model);
|
||||
lines_num++;
|
||||
} while(!text_box_end_of_text_reached(model));
|
||||
lines_num++;
|
||||
|
||||
if(model->focus == TextBoxFocusEnd) {
|
||||
if(lines_num > model->lines_on_screen) {
|
||||
model->text_offset = window_offset[(lines_num - 1) % model->lines_on_screen];
|
||||
}
|
||||
} else {
|
||||
model->text_offset = 0;
|
||||
}
|
||||
|
||||
if(lines_num > model->lines_on_screen) {
|
||||
model->scroll_num = lines_num - model->lines_on_screen;
|
||||
model->scroll_pos = (model->focus == TextBoxFocusEnd) ? model->scroll_num - 1 : 0;
|
||||
}
|
||||
|
||||
text_box_update_screen_text(canvas, model);
|
||||
model->line_offset = model->scroll_pos;
|
||||
}
|
||||
|
||||
static void text_box_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
TextBoxModel* model = _model;
|
||||
|
||||
@@ -132,44 +261,17 @@ static void text_box_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
}
|
||||
|
||||
if(!model->formatted) {
|
||||
text_box_insert_endline(canvas, model);
|
||||
text_box_prepare_model(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 || event->type == InputTypeRepeat) {
|
||||
int32_t scroll_speed = 1;
|
||||
if(text_box->button_held_for_ticks > 5) {
|
||||
if(text_box->button_held_for_ticks % 2) {
|
||||
scroll_speed = 0;
|
||||
} else {
|
||||
scroll_speed = text_box->button_held_for_ticks > 9 ? 5 : 3;
|
||||
}
|
||||
}
|
||||
|
||||
if(event->key == InputKeyDown) {
|
||||
text_box_process_down(text_box, scroll_speed);
|
||||
consumed = true;
|
||||
} else if(event->key == InputKeyUp) {
|
||||
text_box_process_up(text_box, scroll_speed);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
text_box->button_held_for_ticks++;
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
text_box->button_held_for_ticks = 0;
|
||||
consumed = true;
|
||||
if(model->line_offset != model->scroll_pos) {
|
||||
text_box_update_text_on_screen(canvas, model);
|
||||
}
|
||||
return consumed;
|
||||
elements_multiline_text(canvas, 3, 11, furi_string_get_cstr(model->text_on_screen));
|
||||
}
|
||||
|
||||
TextBox* text_box_alloc(void) {
|
||||
@@ -185,7 +287,8 @@ TextBox* text_box_alloc(void) {
|
||||
TextBoxModel * model,
|
||||
{
|
||||
model->text = NULL;
|
||||
model->text_formatted = furi_string_alloc_set("");
|
||||
model->text_on_screen = furi_string_alloc();
|
||||
model->text_line = furi_string_alloc();
|
||||
model->formatted = false;
|
||||
model->font = TextBoxFontText;
|
||||
},
|
||||
@@ -198,7 +301,13 @@ void text_box_free(TextBox* text_box) {
|
||||
furi_check(text_box);
|
||||
|
||||
with_view_model(
|
||||
text_box->view, TextBoxModel * model, { furi_string_free(model->text_formatted); }, true);
|
||||
text_box->view,
|
||||
TextBoxModel * model,
|
||||
{
|
||||
furi_string_free(model->text_on_screen);
|
||||
furi_string_free(model->text_line);
|
||||
},
|
||||
true);
|
||||
view_free(text_box->view);
|
||||
free(text_box);
|
||||
}
|
||||
@@ -216,9 +325,15 @@ void text_box_reset(TextBox* text_box) {
|
||||
TextBoxModel * model,
|
||||
{
|
||||
model->text = NULL;
|
||||
furi_string_set(model->text_formatted, "");
|
||||
model->font = TextBoxFontText;
|
||||
model->focus = TextBoxFocusStart;
|
||||
furi_string_reset(model->text_line);
|
||||
furi_string_reset(model->text_on_screen);
|
||||
model->line_offset = 0;
|
||||
model->text_offset = 0;
|
||||
model->lines_on_screen = 0;
|
||||
model->scroll_num = 0;
|
||||
model->scroll_pos = 0;
|
||||
model->formatted = false;
|
||||
},
|
||||
true);
|
||||
@@ -227,16 +342,12 @@ void text_box_reset(TextBox* text_box) {
|
||||
void text_box_set_text(TextBox* text_box, const char* text) {
|
||||
furi_check(text_box);
|
||||
furi_check(text);
|
||||
size_t str_length = strlen(text);
|
||||
size_t formating_margin = str_length * TEXT_BOX_MAX_SYMBOL_WIDTH / TEXT_BOX_LINE_WIDTH;
|
||||
|
||||
with_view_model(
|
||||
text_box->view,
|
||||
TextBoxModel * model,
|
||||
{
|
||||
model->text = text;
|
||||
furi_string_reset(model->text_formatted);
|
||||
furi_string_reserve(model->text_formatted, str_length + formating_margin);
|
||||
model->formatted = false;
|
||||
},
|
||||
true);
|
||||
|
||||
Reference in New Issue
Block a user