diff --git a/src/widgets/textarea/lv_textarea.c b/src/widgets/textarea/lv_textarea.c index 2b6416db78..86f3a7804e 100644 --- a/src/widgets/textarea/lv_textarea.c +++ b/src/widgets/textarea/lv_textarea.c @@ -65,7 +65,9 @@ static void auto_hide_characters(lv_obj_t * obj); static void auto_hide_characters_cancel(lv_obj_t * obj); static inline bool is_valid_but_non_printable_char(const uint32_t letter); static void lv_textarea_scroll_to_cusor_pos(lv_obj_t * obj, int32_t pos); - +static lv_result_t add_char(lv_obj_t * obj, uint32_t c); +static void add_text(lv_obj_t * obj, const char * txt); +static void set_cursor_pos_internal(lv_obj_t * obj, int32_t pos); /********************** * STATIC VARIABLES **********************/ @@ -183,72 +185,11 @@ lv_obj_t * lv_textarea_create(lv_obj_t * parent) void lv_textarea_add_char(lv_obj_t * obj, uint32_t c) { LV_ASSERT_OBJ(obj, MY_CLASS); - - lv_textarea_t * ta = (lv_textarea_t *)obj; - - if(ta->one_line && (c == '\n' || c == '\r')) { - LV_LOG_INFO("Text area: line break ignored in one-line mode"); + lv_result_t res = add_char(obj, c); + if(res != LV_RESULT_OK) { return; } - - uint32_t u32_buf[2]; - u32_buf[0] = c; - u32_buf[1] = 0; - - const char * letter_buf = (char *)&u32_buf; - - uint32_t c2 = c; -#if LV_BIG_ENDIAN_SYSTEM - if(c != 0) while(*letter_buf == 0) ++letter_buf; - - /*The byte order may or may not need to be swapped here to get correct c_uni below, - since lv_textarea_add_text is ordering bytes correctly before calling lv_textarea_add_char. - Assume swapping is needed if MSB is zero. May not be foolproof. */ - if((c != 0) && ((c & 0xff000000) == 0)) { - c2 = ((c >> 24) & 0xff) | /*move byte 3 to byte 0*/ - ((c << 8) & 0xff0000) | /*move byte 1 to byte 2*/ - ((c >> 8) & 0xff00) | /*move byte 2 to byte 1*/ - ((c << 24) & 0xff000000); /*byte 0 to byte 3*/ - } -#endif - - lv_result_t res = insert_handler(obj, letter_buf); - if(res != LV_RESULT_OK) return; - - uint32_t c_uni = lv_text_encoded_next((const char *)&c2, NULL); - - if(char_is_accepted(obj, c_uni) == false) { - LV_LOG_INFO("Character is not accepted by the text area (too long text or not in the accepted list)"); - return; - } - - if(ta->pwd_mode) pwd_char_hider(obj); /*Make sure all the current text contains only '*'*/ - - /*If the textarea is empty, invalidate it to hide the placeholder*/ - if(ta->placeholder_txt) { - const char * txt = lv_label_get_text(ta->label); - if(txt[0] == '\0') lv_obj_invalidate(obj); - } - - lv_label_ins_text(ta->label, ta->cursor.pos, letter_buf); /*Insert the character*/ - lv_textarea_clear_selection(obj); /*Clear selection*/ - - if(ta->pwd_mode) { - /*+2: the new char + \0*/ - size_t realloc_size = lv_strlen(ta->pwd_tmp) + lv_strlen(letter_buf) + 1; - ta->pwd_tmp = lv_realloc(ta->pwd_tmp, realloc_size); - LV_ASSERT_MALLOC(ta->pwd_tmp); - if(ta->pwd_tmp == NULL) return; - - lv_text_ins(ta->pwd_tmp, ta->cursor.pos, (const char *)letter_buf); - - /*Auto hide characters*/ - auto_hide_characters(obj); - } - - /*Move the cursor after the new character*/ lv_textarea_set_cursor_pos(obj, lv_textarea_get_cursor_pos(obj) + 1); - lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); } @@ -263,11 +204,7 @@ void lv_textarea_add_text(lv_obj_t * obj, const char * txt) /*Add the character one-by-one if not all characters are accepted or there is character limit.*/ if(lv_textarea_get_accepted_chars(obj) || lv_textarea_get_max_length(obj)) { - uint32_t i = 0; - while(txt[i] != '\0') { - uint32_t c = lv_text_encoded_next(txt, &i); - lv_textarea_add_char(obj, lv_text_unicode_to_encoded(c)); - } + add_text(obj, txt); return; } @@ -376,11 +313,7 @@ void lv_textarea_set_text(lv_obj_t * obj, const char * txt) if(ta->pwd_mode) { ta->pwd_tmp[0] = '\0'; /*Clear the password too*/ } - uint32_t i = 0; - while(txt[i] != '\0') { - uint32_t c = lv_text_encoded_next(txt, &i); - lv_textarea_add_char(obj, lv_text_unicode_to_encoded(c)); - } + add_text(obj, txt); } else { lv_label_set_text(ta->label, txt); @@ -401,8 +334,6 @@ void lv_textarea_set_text(lv_obj_t * obj, const char * txt) pwd_char_hider(obj); } - - lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); } void lv_textarea_set_placeholder_text(lv_obj_t * obj, const char * txt) @@ -437,17 +368,7 @@ void lv_textarea_set_placeholder_text(lv_obj_t * obj, const char * txt) void lv_textarea_set_cursor_pos(lv_obj_t * obj, int32_t pos) { LV_ASSERT_OBJ(obj, MY_CLASS); - - lv_textarea_t * ta = (lv_textarea_t *)obj; - if((uint32_t)ta->cursor.pos == (uint32_t)pos) return; - - uint32_t len = lv_text_get_encoded_length(lv_label_get_text(ta->label)); - - if(pos < 0) pos = len + pos; - - if(pos > (int32_t)len || pos == LV_TEXTAREA_CURSOR_LAST) pos = len; - - ta->cursor.pos = pos; + set_cursor_pos_internal(obj, pos); /*Position the label to make the cursor visible*/ lv_obj_update_layout(obj); @@ -1530,4 +1451,117 @@ static void lv_textarea_scroll_to_cusor_pos(lv_obj_t * obj, int32_t pos) refr_cursor_area(obj); } +static void set_cursor_pos_internal(lv_obj_t * obj, int32_t pos) +{ + lv_textarea_t * ta = (lv_textarea_t *)obj; + if((uint32_t)ta->cursor.pos == (uint32_t)pos) return; + + uint32_t len = lv_text_get_encoded_length(lv_label_get_text(ta->label)); + + if(pos < 0) pos = len + pos; + + if(pos > (int32_t)len || pos == LV_TEXTAREA_CURSOR_LAST) pos = len; + + ta->cursor.pos = pos; +} + +static void add_text(lv_obj_t * obj, const char * txt) +{ + lv_textarea_t * ta = (lv_textarea_t *)obj; + uint32_t i = 0; + bool text_changed = false; + while(txt[i] != '\0') { + uint32_t c = lv_text_encoded_next(txt, &i); + lv_result_t res = add_char(obj, lv_text_unicode_to_encoded(c)); + if(res != LV_RESULT_OK) { + continue; + } + set_cursor_pos_internal(obj, ta->cursor.pos + 1); + text_changed = true; + } + + if(!text_changed) { + return; + } + + lv_obj_update_layout(obj); + lv_textarea_scroll_to_cusor_pos(obj, ta->cursor.pos); + refr_cursor_area(obj); + /*Move the cursor after the new character*/ + lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); + +} +static lv_result_t add_char(lv_obj_t * obj, uint32_t c) +{ + LV_ASSERT_OBJ(obj, MY_CLASS); + + lv_textarea_t * ta = (lv_textarea_t *)obj; + + if(ta->one_line && (c == '\n' || c == '\r')) { + LV_LOG_INFO("Text area: line break ignored in one-line mode"); + return LV_RESULT_INVALID; + } + + uint32_t u32_buf[2]; + u32_buf[0] = c; + u32_buf[1] = 0; + + const char * letter_buf = (char *)&u32_buf; + + uint32_t c2 = c; +#if LV_BIG_ENDIAN_SYSTEM + if(c != 0) while(*letter_buf == 0) ++letter_buf; + + /*The byte order may or may not need to be swapped here to get correct c_uni below, + since lv_textarea_add_text is ordering bytes correctly before calling lv_textarea_add_char. + Assume swapping is needed if MSB is zero. May not be foolproof. */ + if((c != 0) && ((c & 0xff000000) == 0)) { + c2 = ((c >> 24) & 0xff) | /*move byte 3 to byte 0*/ + ((c << 8) & 0xff0000) | /*move byte 1 to byte 2*/ + ((c >> 8) & 0xff00) | /*move byte 2 to byte 1*/ + ((c << 24) & 0xff000000); /*byte 0 to byte 3*/ + } +#endif + + lv_result_t res = insert_handler(obj, letter_buf); + if(res != LV_RESULT_OK) return res; + + uint32_t c_uni = lv_text_encoded_next((const char *)&c2, NULL); + + if(char_is_accepted(obj, c_uni) == false) { + LV_LOG_INFO("Character is not accepted by the text area (too long text or not in the accepted list)"); + return LV_RESULT_INVALID; + } + + if(ta->pwd_mode) pwd_char_hider(obj); /*Make sure all the current text contains only '*'*/ + + /*If the textarea is empty, invalidate it to hide the placeholder*/ + if(ta->placeholder_txt) { + const char * txt = lv_label_get_text(ta->label); + if(txt[0] == '\0') lv_obj_invalidate(obj); + } + + /* Try to allocate memory for pwd mode first, if we fail we don't insert the character either*/ + if(ta->pwd_mode) { + /*+2: the new char + \0*/ + size_t realloc_size = lv_strlen(ta->pwd_tmp) + lv_strlen(letter_buf) + 1; + char * pwd_tmp = lv_realloc(ta->pwd_tmp, realloc_size); + LV_ASSERT_MALLOC(pwd_tmp); + if(!pwd_tmp) return LV_RESULT_INVALID; + ta->pwd_tmp = pwd_tmp; + } + + + lv_label_ins_text(ta->label, ta->cursor.pos, letter_buf); /*Insert the character*/ + lv_textarea_clear_selection(obj); /*Clear selection*/ + + if(ta->pwd_mode) { + lv_text_ins(ta->pwd_tmp, ta->cursor.pos, (const char *)letter_buf); + + /*Auto hide characters*/ + auto_hide_characters(obj); + } + return LV_RESULT_OK; +} + #endif diff --git a/tests/src/test_cases/widgets/test_textarea.c b/tests/src/test_cases/widgets/test_textarea.c index f331b32431..e486a04de1 100644 --- a/tests/src/test_cases/widgets/test_textarea.c +++ b/tests/src/test_cases/widgets/test_textarea.c @@ -233,4 +233,85 @@ void test_textarea_properties(void) #endif } +static uint32_t event_count; +static void event_counter_cb(lv_event_t * e) +{ + LV_UNUSED(e); + event_count++; +} + +void test_textarea_set_text_should_emit_value_changed_event_only_once(void) +{ + + const char * accepted_list = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ!,"; + const char * text = "Hello, World!"; + const uint32_t text_len = 13U; /* strlen("Hello, World!") */ + + /* Test 1: with accepted_chars set */ + event_count = 0; + lv_textarea_set_accepted_chars(textarea, accepted_list); + lv_obj_add_event_cb(textarea, event_counter_cb, LV_EVENT_VALUE_CHANGED, NULL); + + lv_textarea_set_text(textarea, text); + + TEST_ASSERT_EQUAL_STRING(text, lv_textarea_get_text(textarea)); + TEST_ASSERT_EQUAL_UINT32(1U, event_count); + + /* Test 2: with max_length set to exactly the text length — if set_text + * doesn't clear before re-adding chars, char_is_accepted sees the buffer + * as already full and rejects every character, leaving the textarea empty */ + lv_obj_clean(active_screen); + textarea = lv_textarea_create(active_screen); + + event_count = 0; + lv_textarea_set_max_length(textarea, text_len); + lv_obj_add_event_cb(textarea, event_counter_cb, LV_EVENT_VALUE_CHANGED, NULL); + + lv_textarea_set_text(textarea, text); + + TEST_ASSERT_EQUAL_STRING(text, lv_textarea_get_text(textarea)); + TEST_ASSERT_EQUAL_UINT32(1U, event_count); + + /* Test 3: with both accepted_chars and max_length set */ + lv_obj_clean(active_screen); + textarea = lv_textarea_create(active_screen); + + event_count = 0; + lv_textarea_set_accepted_chars(textarea, accepted_list); + lv_textarea_set_max_length(textarea, text_len); + lv_obj_add_event_cb(textarea, event_counter_cb, LV_EVENT_VALUE_CHANGED, NULL); + + lv_textarea_set_text(textarea, text); + + TEST_ASSERT_EQUAL_STRING(text, lv_textarea_get_text(textarea)); + TEST_ASSERT_EQUAL_UINT32(1U, event_count); + + /* Test 4: empty string — no characters to add, no event */ + lv_obj_clean(active_screen); + textarea = lv_textarea_create(active_screen); + + event_count = 0; + lv_textarea_set_accepted_chars(textarea, accepted_list); + lv_textarea_set_max_length(textarea, text_len); + lv_obj_add_event_cb(textarea, event_counter_cb, LV_EVENT_VALUE_CHANGED, NULL); + + lv_textarea_set_text(textarea, ""); + + TEST_ASSERT_EQUAL_STRING("", lv_textarea_get_text(textarea)); + TEST_ASSERT_EQUAL_UINT32(0U, event_count); + + /* Test 5: all characters rejected by accepted_chars — no characters added, no event */ + lv_obj_clean(active_screen); + textarea = lv_textarea_create(active_screen); + + event_count = 0; + lv_textarea_set_accepted_chars(textarea, accepted_list); + lv_obj_add_event_cb(textarea, event_counter_cb, LV_EVENT_VALUE_CHANGED, NULL); + + lv_textarea_set_text(textarea, "123"); /* digits not in accepted_list */ + + TEST_ASSERT_EQUAL_STRING("", lv_textarea_get_text(textarea)); + TEST_ASSERT_EQUAL_UINT32(0U, event_count); +} + #endif