diff --git a/examples/widgets/span/lv_example_span_1.c b/examples/widgets/span/lv_example_span_1.c index f74e0a3f8..26466a9c7 100644 --- a/examples/widgets/span/lv_example_span_1.c +++ b/examples/widgets/span/lv_example_span_1.c @@ -1,8 +1,19 @@ #include "../../lv_examples.h" #if LV_USE_SPAN && LV_BUILD_EXAMPLES +static void click_event_cb(lv_event_t * e) +{ + lv_obj_t * spans = lv_event_get_target(e); + lv_indev_t * indev = lv_event_get_indev(e); + lv_point_t point; + lv_indev_get_point(indev, &point); + lv_span_t * span = lv_spangroup_get_span_by_point(spans, &point); + + LV_LOG_USER("%s", span ? lv_span_get_text(span) : "NULL"); +} + /** - * Create span. + * Create spans and get clicked one */ void lv_example_span_1(void) { @@ -17,6 +28,7 @@ void lv_example_span_1(void) lv_obj_set_height(spans, 300); lv_obj_center(spans); lv_obj_add_style(spans, &style, 0); + lv_obj_add_flag(spans, LV_OBJ_FLAG_CLICKABLE); lv_spangroup_set_align(spans, LV_TEXT_ALIGN_LEFT); lv_spangroup_set_overflow(spans, LV_SPAN_OVERFLOW_CLIP); @@ -53,6 +65,8 @@ void lv_example_span_1(void) lv_style_set_text_decor(lv_span_get_style(span), LV_TEXT_DECOR_STRIKETHROUGH); lv_spangroup_refr_mode(spans); + + lv_obj_add_event_cb(spans, click_event_cb, LV_EVENT_CLICKED, NULL); } #endif diff --git a/src/widgets/span/lv_span.c b/src/widgets/span/lv_span.c index c4afb4373..a942dba1e 100644 --- a/src/widgets/span/lv_span.c +++ b/src/widgets/span/lv_span.c @@ -69,6 +69,9 @@ static void lv_snippet_push(lv_snippet_t * item); static lv_snippet_t * lv_get_snippet(uint32_t index); static int32_t convert_indent_pct(lv_obj_t * spans, int32_t width); +static lv_span_coords_t make_span_coords(const lv_span_t * prev_span, const lv_span_t * curr_span, int32_t width, + lv_area_t padding, int32_t indent); + /********************** * STATIC VARIABLES **********************/ @@ -258,6 +261,11 @@ lv_style_t * lv_span_get_style(lv_span_t * span) return &span->style; } +const char * lv_span_get_text(lv_span_t * span) +{ + return span->txt; +} + lv_span_t * lv_spangroup_get_child(const lv_obj_t * obj, int32_t id) { if(obj == NULL) { @@ -451,6 +459,7 @@ int32_t lv_spangroup_get_expand_height(lv_obj_t * obj, int32_t width) lv_snippet_t snippet; /* use to save cur_span info and push it to stack */ lv_memset(&snippet, 0, sizeof(snippet)); + lv_span_t * prev_span = cur_span; int32_t line_cnt = 0; int32_t lines = spans->lines < 0 ? INT32_MAX : spans->lines; /* the loop control how many lines need to draw */ @@ -462,6 +471,8 @@ int32_t lv_spangroup_get_expand_height(lv_obj_t * obj, int32_t width) while(1) { /* switch to the next span when current is end */ if(cur_txt[cur_txt_ofs] == '\0') { + cur_span->trailing_pos = txt_pos; + cur_span = lv_ll_get_next(&spans->child_ll, cur_span); if(cur_span == NULL) break; cur_txt = cur_span->txt; @@ -484,6 +495,8 @@ int32_t lv_spangroup_get_expand_height(lv_obj_t * obj, int32_t width) int32_t use_width = 0; bool isfill = lv_text_get_snippet(&cur_txt[cur_txt_ofs], snippet.font, snippet.letter_space, max_w, txt_flag, &use_width, &next_ofs); + if(isfill) txt_pos.x = 0; + else txt_pos.x += use_width; /* break word deal width */ if(isfill && next_ofs > 0 && snippet_cnt > 0) { @@ -521,8 +534,16 @@ int32_t lv_spangroup_get_expand_height(lv_obj_t * obj, int32_t width) } /* next line init */ - txt_pos.x = 0; txt_pos.y += max_line_h; + + /* iterate all the spans in the current line and set the trailing height to the max line height */ + for(lv_span_t * tmp_span = prev_span; + tmp_span && tmp_span != cur_span; + tmp_span = lv_ll_get_next(&spans->child_ll, tmp_span)) + tmp_span->trailing_height = max_line_h; + + prev_span = cur_span; + max_w = max_width; line_cnt += 1; if(line_cnt >= lines) { @@ -534,6 +555,69 @@ int32_t lv_spangroup_get_expand_height(lv_obj_t * obj, int32_t width) return txt_pos.y; } +lv_span_coords_t lv_spangroup_get_span_coords(lv_obj_t * obj, const lv_span_t * span) +{ + /* find previous span */ + const lv_spangroup_t * spangroup = (lv_spangroup_t *)obj; + const lv_ll_t * spans = &spangroup->child_ll; + const int32_t width = lv_obj_get_content_width(obj); + const int32_t indent = lv_spangroup_get_indent(obj); + + if(obj == NULL || span == NULL || lv_ll_get_head(spans) == NULL) return (lv_span_coords_t) { + 0 + }; + + lv_span_t * prev_span = NULL; + lv_span_t * curr_span; + LV_LL_READ(spans, curr_span) { + if(curr_span == span) break; + prev_span = curr_span; + } + + const uint32_t border_width = lv_obj_get_style_border_width(obj, LV_PART_MAIN); + return make_span_coords(prev_span, curr_span, width, (lv_area_t) { + .x1 = lv_obj_get_style_pad_left(obj, LV_PART_MAIN) + border_width, + .y1 = lv_obj_get_style_pad_top(obj, LV_PART_MAIN) + border_width, + .x2 = lv_obj_get_style_pad_right(obj, LV_PART_MAIN) + border_width, .y2 = 0 + }, + indent); +} + +lv_span_t * lv_spangroup_get_span_by_point(lv_obj_t * obj, const lv_point_t * p) +{ + const lv_spangroup_t * spangroup = (lv_spangroup_t *)obj; + const lv_ll_t * spans = &spangroup->child_ll; + const int32_t width = lv_obj_get_content_width(obj); + const int32_t indent = lv_spangroup_get_indent(obj); + + if(obj == NULL || p == NULL || lv_ll_get_head(spans) == NULL) return NULL; + + lv_point_t point; + point.x = p->x - obj->coords.x1; + point.y = p->y - obj->coords.y1; + + /* find previous span */ + + const lv_span_t * prev_span = NULL; + lv_span_t * curr_span; + LV_LL_READ(spans, curr_span) { + lv_span_coords_t coords = make_span_coords(prev_span, curr_span, width, (lv_area_t) { + .x1 = lv_obj_get_style_pad_left(obj, LV_PART_MAIN), + .y1 = lv_obj_get_style_pad_top(obj, LV_PART_MAIN), + .x2 = lv_obj_get_style_pad_right(obj, LV_PART_MAIN), + .y2 = 0 + }, + indent); + if(lv_area_is_point_on(&coords.heading, &point, 0) || + lv_area_is_point_on(&coords.middle, &point, 0) || + lv_area_is_point_on(&coords.trailing, &point, 0)) { + return curr_span; + } + prev_span = curr_span; + } + return NULL; +} + /********************** * STATIC FUNCTIONS **********************/ @@ -1086,4 +1170,52 @@ static void refresh_self_size(lv_obj_t * obj) lv_obj_refresh_self_size(obj); } +static lv_span_coords_t make_span_coords(const lv_span_t * prev_span, const lv_span_t * curr_span, const int32_t width, + const lv_area_t padding, const int32_t indent) +{ + lv_span_coords_t coords = { 0 }; + + if(curr_span == NULL) return coords; + + /* first line */ + if(prev_span == NULL) { + lv_area_set(&coords.heading, padding.x1 + indent, padding.y1, width + padding.x1, + curr_span->trailing_pos.y + padding.y1); + lv_area_set(&coords.middle, coords.heading.x1, coords.heading.y2, curr_span->trailing_pos.x + padding.x1, + coords.heading.y2 + curr_span->trailing_height); + lv_area_set(&coords.trailing, 0, 0, 0, 0); + + return coords; + } + + /* start and end on the same line */ + const bool is_same_line = prev_span->trailing_pos.y == curr_span->trailing_pos.y; + if(is_same_line == true) { + lv_area_set(&coords.heading, + prev_span->trailing_pos.x + padding.x1, prev_span->trailing_pos.y + padding.y1, + curr_span->trailing_pos.x + padding.x1, curr_span->trailing_pos.y + curr_span->trailing_height + padding.y1); + return coords; + } + + /* common case */ + const lv_point_t pre_trailing_pos = prev_span->trailing_pos; + const int32_t pre_trailing_height = prev_span->trailing_height; + + lv_area_set(&coords.heading, + pre_trailing_pos.x + padding.x1, pre_trailing_pos.y + padding.y1, + width + padding.x1, pre_trailing_pos.y + pre_trailing_height + padding.y1); + /* When it happens to be two lines of text, + * the y2 of the middle area is exactly the y1 + line height of the first line of text, + * so the area of the middle area is empty. + * */ + lv_area_set(&coords.middle, + padding.x1, coords.heading.y2, + width + padding.x1, curr_span->trailing_pos.y + padding.y1); + lv_area_set(&coords.trailing, + coords.middle.x1, coords.middle.y2, + curr_span->trailing_pos.x + padding.x1, curr_span->trailing_pos.y + curr_span->trailing_height + padding.y1); + + return coords; +} + #endif diff --git a/src/widgets/span/lv_span.h b/src/widgets/span/lv_span.h index f0406b918..17bcf51e3 100644 --- a/src/widgets/span/lv_span.h +++ b/src/widgets/span/lv_span.h @@ -41,6 +41,13 @@ typedef enum { LV_SPAN_MODE_LAST /**< Fence member */ } lv_span_mode_t; +/** Coords of a span */ +typedef struct _lv_span_coords_t { + lv_area_t heading; + lv_area_t middle; + lv_area_t trailing; +} lv_span_coords_t; + LV_ATTRIBUTE_EXTERN_DATA extern const lv_obj_class_t lv_spangroup_class; /********************** @@ -136,6 +143,13 @@ void lv_spangroup_set_max_lines(lv_obj_t * obj, int32_t lines); */ lv_style_t * lv_span_get_style(lv_span_t * span); +/** + * Get a pointer to the text of a span + * @param span pointer to the span + * @return pointer to the text +*/ +const char * lv_span_get_text(lv_span_t * span); + /** * Get a spangroup child by its index. * @@ -214,6 +228,39 @@ uint32_t lv_spangroup_get_expand_width(lv_obj_t * obj, uint32_t max_width); */ int32_t lv_spangroup_get_expand_height(lv_obj_t * obj, int32_t width); +/** + * Get the span's coords in the spangroup. + * @note Before calling this function, please make sure that the layout of span group has been updated. + * Like calling lv_obj_update_layout() like function. + * + * +--------+ + * |Heading +--->------------------+ + * | Pos | | Heading | + * +--------+---+------------------+ + * | | + * | | + * | | + * | Middle +--------+| + * | |Trailing|| + * | +-| Pos || + * | | +--------+| + * +-------------------v-----------+ + * | Trailing | + * +-------------------+ + * @param obj pointer to a spangroup object. + * @param span pointer to a span. + * @return the span's coords in the spangroup. + */ +lv_span_coords_t lv_spangroup_get_span_coords(lv_obj_t * obj, const lv_span_t * span); + +/** + * Get the span object by point. + * @param obj pointer to a spangroup object. + * @param point pointer to point containing absolute coordinates + * @return pointer to the span under the point or `NULL` if not found. + */ +lv_span_t * lv_spangroup_get_span_by_point(lv_obj_t * obj, const lv_point_t * point); + /*===================== * Other functions *====================*/ diff --git a/src/widgets/span/lv_span_private.h b/src/widgets/span/lv_span_private.h index 471b50db4..6e80405ef 100644 --- a/src/widgets/span/lv_span_private.h +++ b/src/widgets/span/lv_span_private.h @@ -32,6 +32,9 @@ struct _lv_span_t { lv_obj_t * spangroup; /**< a pointer to spangroup */ lv_style_t style; /**< display text style */ uint32_t static_flag : 1; /**< the text is static flag */ + + lv_point_t trailing_pos; + int32_t trailing_height; }; /** Data of label*/ diff --git a/tests/ref_imgs/widgets/span_09.png b/tests/ref_imgs/widgets/span_09.png new file mode 100644 index 000000000..2ad195e70 Binary files /dev/null and b/tests/ref_imgs/widgets/span_09.png differ diff --git a/tests/src/test_cases/widgets/test_span.c b/tests/src/test_cases/widgets/test_span.c index 44ee88690..5cbc8c223 100644 --- a/tests/src/test_cases/widgets/test_span.c +++ b/tests/src/test_cases/widgets/test_span.c @@ -375,4 +375,108 @@ void test_spangroup_style_text_letter_space(void) TEST_ASSERT_EQUAL_SCREENSHOT("widgets/span_08.png"); } +#if LV_FONT_MONTSERRAT_24 && LV_FONT_MONTSERRAT_20 +void test_spangroup_get_span_coords(void) +{ + /* Initialize the active screen and create a new span group */ + active_screen = lv_screen_active(); + spangroup = lv_spangroup_create(active_screen); + + const uint32_t span_count = 5; + lv_span_t * spans[span_count]; + + /* Set styles and properties for the span group */ + lv_obj_set_style_outline_width(spangroup, 1, 0); + lv_spangroup_set_indent(spangroup, 20); + lv_spangroup_set_mode(spangroup, LV_SPAN_MODE_BREAK); + lv_obj_set_width(spangroup, 300); + lv_obj_set_style_pad_all(spangroup, 20, LV_PART_MAIN); + + /* Create spans and set their properties */ + spans[0] = lv_spangroup_new_span(spangroup); + lv_span_set_text(spans[0], "China is a beautiful country."); + lv_style_set_text_color(lv_span_get_style(spans[0]), lv_palette_main(LV_PALETTE_RED)); + lv_style_set_text_decor(lv_span_get_style(spans[0]), LV_TEXT_DECOR_UNDERLINE); + lv_style_set_text_opa(lv_span_get_style(spans[0]), LV_OPA_50); + + spans[1] = lv_spangroup_new_span(spangroup); + lv_span_set_text_static(spans[1], "good good study, day day up."); + lv_style_set_text_font(lv_span_get_style(spans[1]), &lv_font_montserrat_24); + lv_style_set_text_color(lv_span_get_style(spans[1]), lv_palette_main(LV_PALETTE_GREEN)); + + spans[2] = lv_spangroup_new_span(spangroup); + lv_span_set_text_static(spans[2], "LVGL is an open-source graphics library."); + lv_style_set_text_color(lv_span_get_style(spans[2]), lv_palette_main(LV_PALETTE_BLUE)); + + spans[3] = lv_spangroup_new_span(spangroup); + lv_span_set_text_static(spans[3], "the boy no name."); + lv_style_set_text_color(lv_span_get_style(spans[3]), lv_palette_main(LV_PALETTE_GREEN)); + lv_style_set_text_font(lv_span_get_style(spans[3]), &lv_font_montserrat_20); + lv_style_set_text_decor(lv_span_get_style(spans[3]), LV_TEXT_DECOR_UNDERLINE); + + spans[4] = lv_spangroup_new_span(spangroup); + lv_span_set_text(spans[4], "I have a dream that hope to come true."); + lv_style_set_text_decor(lv_span_get_style(spans[4]), LV_TEXT_DECOR_STRIKETHROUGH); + + /* Refresh the span group mode and update layout */ + lv_spangroup_refr_mode(spangroup); + lv_obj_update_layout(spangroup); + + /* Define expected coordinates for testing */ + const lv_span_coords_t test_coords[] = { + {.heading = {.x1 = 40, .y1 = 20, .x2 = 280, .y2 = 20}, .middle = {.x1 = 40, .y1 = 20, .x2 = 241, .y2 = 36}, .trailing = {.x1 = 0, .y1 = 0, .x2 = 0, .y2 = 0}}, + {.heading = {.x1 = 241, .y1 = 20, .x2 = 280, .y2 = 36}, .middle = {.x1 = 20, .y1 = 36, .x2 = 280, .y2 = 63}, .trailing = {.x1 = 20, .y1 = 63, .x2 = 155, .y2 = 90}}, + {.heading = {.x1 = 155, .y1 = 63, .x2 = 280, .y2 = 90}, .middle = {.x1 = 20, .y1 = 90, .x2 = 280, .y2 = 90}, .trailing = {.x1 = 20, .y1 = 90, .x2 = 188, .y2 = 112}}, + {.heading = {.x1 = 188, .y1 = 90, .x2 = 280, .y2 = 112}, .middle = {.x1 = 20, .y1 = 112, .x2 = 280, .y2 = 112}, .trailing = {.x1 = 20, .y1 = 112, .x2 = 116, .y2 = 134}}, + {.heading = {.x1 = 116, .y1 = 112, .x2 = 280, .y2 = 134}, .middle = {.x1 = 20, .y1 = 134, .x2 = 280, .y2 = 134}, .trailing = {.x1 = 20, .y1 = 134, .x2 = 160, .y2 = 150}} + }; + + /* Define colors for visual testing */ + const lv_color_t colors[] = { + lv_palette_main(LV_PALETTE_RED), lv_palette_main(LV_PALETTE_GREEN), lv_palette_main(LV_PALETTE_BLUE), + lv_palette_main(LV_PALETTE_YELLOW), lv_palette_main(LV_PALETTE_PURPLE), lv_palette_main(LV_PALETTE_ORANGE), + lv_palette_main(LV_PALETTE_INDIGO), lv_palette_main(LV_PALETTE_BROWN), lv_palette_main(LV_PALETTE_GREY), + lv_palette_main(LV_PALETTE_PINK) + }; + const uint32_t color_count = sizeof(colors) / sizeof(colors[0]); + const lv_area_t area = spangroup->coords; + + /* Iterate through spans and validate coordinates */ + for(uint32_t i = 0; i < span_count; i++) { + lv_span_coords_t coords = lv_spangroup_get_span_coords(spangroup, spans[i]); + TEST_ASSERT_EQUAL_MEMORY(&coords.heading, &test_coords[i].heading, sizeof(lv_span_coords_t)); + + /* Visual testing */ + const lv_color_t color = colors[i % color_count]; + + /* Create and style heading object */ + lv_obj_t * obj_head = lv_obj_create(active_screen); + lv_obj_remove_style_all(obj_head); + lv_obj_set_pos(obj_head, coords.heading.x1 + area.x1, coords.heading.y1 + area.y1); + lv_obj_set_size(obj_head, coords.heading.x2 - coords.heading.x1, coords.heading.y2 - coords.heading.y1); + lv_obj_set_style_bg_color(obj_head, color, LV_PART_MAIN); + lv_obj_set_style_bg_opa(obj_head, LV_OPA_50, LV_PART_MAIN); + + /* Create and style middle object */ + lv_obj_t * obj_middle = lv_obj_create(active_screen); + lv_obj_remove_style_all(obj_middle); + lv_obj_set_pos(obj_middle, coords.middle.x1 + area.x1, coords.middle.y1 + area.y1); + lv_obj_set_size(obj_middle, coords.middle.x2 - coords.middle.x1, coords.middle.y2 - coords.middle.y1); + lv_obj_set_style_bg_color(obj_middle, color, LV_PART_MAIN); + lv_obj_set_style_bg_opa(obj_middle, LV_OPA_50, LV_PART_MAIN); + + /* Create and style trailing object */ + lv_obj_t * obj_trailing = lv_obj_create(active_screen); + lv_obj_remove_style_all(obj_trailing); + lv_obj_set_pos(obj_trailing, coords.trailing.x1 + area.x1, coords.trailing.y1 + area.y1); + lv_obj_set_size(obj_trailing, coords.trailing.x2 - coords.trailing.x1, coords.trailing.y2 - coords.trailing.y1); + lv_obj_set_style_bg_color(obj_trailing, color, LV_PART_MAIN); + lv_obj_set_style_bg_opa(obj_trailing, LV_OPA_50, LV_PART_MAIN); + } + + /* Validate the final screenshot */ + TEST_ASSERT_EQUAL_SCREENSHOT("widgets/span_09.png"); +} +#endif + #endif