feat(textarea): only send one event when updating text (#9852)
Arduino Lint / lint (push) Has been cancelled
Build Examples with C++ Compiler / build-examples (push) Has been cancelled
MicroPython CI / Build esp32 port (push) Has been cancelled
MicroPython CI / Build rp2 port (push) Has been cancelled
MicroPython CI / Build stm32 port (push) Has been cancelled
MicroPython CI / Build unix port (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_NORMAL_8BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_SDL - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build ESP IDF ESP32S3 (push) Has been cancelled
C/C++ CI / Run tests with 32bit build (push) Has been cancelled
C/C++ CI / Run tests with 64bit build (push) Has been cancelled
BOM Check / bom-check (push) Has been cancelled
Verify that lv_conf_internal.h matches repository state / verify-conf-internal (push) Has been cancelled
Verify GDB constants are up-to-date / verify-gdb-consts (push) Has been cancelled
Verify the widget property name / verify-property-name (push) Has been cancelled
Verify code formatting / verify-formatting (push) Has been cancelled
Compare file templates with file names / template-check (push) Has been cancelled
Build docs / build-and-deploy (push) Has been cancelled
Test API JSON generator / Test API JSON (push) Has been cancelled
Install LVGL using CMake / build-examples (push) Has been cancelled
Check Makefile / Build using Makefile (push) Has been cancelled
Check Makefile for UEFI / Build using Makefile for UEFI (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/benchmark_results_comment/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/filter_docker_logs/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/serialize_results/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 32b - lv_conf_perf32b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 64b - lv_conf_perf64b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Save PR Number (push) Has been cancelled
Hardware Performance Test / Hardware Performance Benchmark (push) Has been cancelled
Hardware Performance Test / HW Benchmark - Save PR Number (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_32B - Ubuntu (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_64B - Ubuntu (push) Has been cancelled
Port repo release update / run-release-branch-updater (push) Has been cancelled
Verify Font License / verify-font-license (push) Has been cancelled
Verify Kconfig / verify-kconfig (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
André Costa
2026-03-23 19:29:58 +01:00
committed by GitHub
parent 6b05d6f5d7
commit 012c98793d
2 changed files with 202 additions and 87 deletions
+121 -87
View File
@@ -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
@@ -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