#include "bubble.h" typedef struct { float min_val; float max_val; float cur_val; } bubble_level_dsc_t; static void bubble_draw_cb(lv_event_t * e) { lvgl_port_lock(0); lv_obj_t * obj = lv_event_get_target(e); bubble_level_dsc_t * d = lv_obj_get_user_data(obj); if(!d) return; // 1) Get the LVGL draw layer (clipping is automatic) lv_layer_t * layer = lv_event_get_layer(e); // 2) Figure out the object's inner rectangle lv_area_t coords; lv_obj_get_coords(obj, &coords); lv_area_t inner = { .x1 = coords.x1 + 2, .y1 = coords.y1 + 2, .x2 = coords.x2 - 2, .y2 = coords.y2 - 2 }; // 3) Draw the border lv_draw_rect_dsc_t rect_dsc; lv_draw_rect_dsc_init(&rect_dsc); rect_dsc.border_color = lv_color_hex(0xAAAAAA); rect_dsc.border_width = 2; rect_dsc.bg_opa = LV_OPA_TRANSP; lv_draw_rect(layer, &rect_dsc, &inner); // 4) Draw guide lines at the center lv_coord_t mid_x = (inner.x1 + inner.x2) / 2; lv_coord_t mid_y = (inner.y1 + inner.y2) / 2; lv_draw_line_dsc_t line_dsc; lv_draw_line_dsc_init(&line_dsc); line_dsc.color = lv_color_hex(0x444444); line_dsc.width = 1; // Set up points and convert to 'precise' type lv_point_t tl = { inner.x1 + 10, mid_y }; lv_point_t tr = { inner.x2 - 10, mid_y }; line_dsc.p1 = lv_point_to_precise(&tl); line_dsc.p2 = lv_point_to_precise(&tr); lv_draw_line(layer, &line_dsc); lv_point_t tt = { mid_x, inner.y1 + 10 }; lv_point_t tb = { mid_x, inner.y2 - 10 }; line_dsc.p1 = lv_point_to_precise(&tt); line_dsc.p2 = lv_point_to_precise(&tb); lv_draw_line(layer, &line_dsc); // 5) Compute bubble position (normalized between min_val → max_val) float norm = (d->cur_val - d->min_val) / (d->max_val - d->min_val); norm = norm < 0 ? 0 : (norm > 1 ? 1 : norm); const lv_coord_t bubble_d = 16; lv_coord_t avail_w = (inner.x2 - inner.x1 + 1) - bubble_d; lv_coord_t bx = inner.x1 + (lv_coord_t)(norm * avail_w); lv_coord_t by = mid_y - (bubble_d / 2); // 6) Draw the bubble as a filled circle (rounded rect) lv_draw_rect_dsc_t circ_dsc; lv_draw_rect_dsc_init(&circ_dsc); circ_dsc.bg_color = lv_color_hex(0x00CC00); circ_dsc.bg_opa = LV_OPA_COVER; circ_dsc.radius = bubble_d / 2; circ_dsc.border_opa = LV_OPA_TRANSP; lv_area_t circ_area = { .x1 = bx, .y1 = by, .x2 = bx + bubble_d - 1, .y2 = by + bubble_d - 1 }; lv_draw_rect(layer, &circ_dsc, &circ_area); lvgl_port_unlock(); } lv_obj_t * bubble_create(lv_obj_t * parent, lv_coord_t width, lv_coord_t height, float min_val, float max_val, float initial) { // 1) Create a container object (no special style) lv_obj_t * obj = lv_obj_create(parent); lv_obj_set_size(obj, width, height); // (Optional) center it or let the caller position it: // lv_obj_center(obj); // 2) Allocate or attach our descriptor // In LVGL v9 you can use USER_DATA: need to enable LV_USE_USER_DATA = 1 bubble_level_dsc_t * dsc = lv_malloc(sizeof(bubble_level_dsc_t)); if(!dsc) return NULL; /* handle out-of-memory */ dsc->min_val = min_val; dsc->max_val = max_val; dsc->cur_val = initial; lv_obj_set_user_data(obj, dsc); // 3) Register our draw callback lv_obj_add_event_cb(obj, bubble_draw_cb, LV_EVENT_DRAW_MAIN, NULL); // 4) Make sure the object does NOT get a default background fill lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); return obj; } void bubble_setValue(lv_obj_t * bubble, float v) { lvgl_port_lock(0); // 1) Retrieve the descriptor bubble_level_dsc_t * d = lv_obj_get_user_data(bubble); if(!d) return; // 2) Update the stored value d->cur_val = v; // 3) Tell LVGL that object must be re‐drawn lv_obj_invalidate(bubble); lvgl_port_unlock(); }