diff --git a/docs/src/common-widget-features/api.mdx b/docs/src/common-widget-features/api.mdx index 397f4ef068..6005da610a 100644 --- a/docs/src/common-widget-features/api.mdx +++ b/docs/src/common-widget-features/api.mdx @@ -121,3 +121,31 @@ void some_timer_callback(lv_timer_t * t) } } ``` + +### Delete Callbacks + +If you need to perform cleanup when a widget is deleted, you can register a delete +callback using . +The callback will be automatically invoked with `user_data` as its argument when the widget +is deleted. + +This is a convenience utility built on top of , useful for freeing +resources that are tied to a widget's lifetime without manually managing event handlers. + +The following example automatically deletes a timer when the widget is deleted: + +```c +lv_timer_t * timer = lv_timer_create_basic(); +lv_obj_add_delete_cb(obj, (lv_delete_cb_t)lv_timer_delete, timer); +``` + +The following example frees a heap-allocated buffer when the widget is deleted: + +```c +void * my_data = malloc(64); +lv_obj_add_delete_cb(obj, free, my_data); +``` + + returns a pointer to the created +descriptor, which can be used to remove the callback later if needed via . +Multiple delete callbacks can be attached to the same widget, and all of them will be called when the widget is deleted. diff --git a/include/lvgl/core/lv_obj.h b/include/lvgl/core/lv_obj.h index cc31f50aeb..81ce0e9661 100644 --- a/include/lvgl/core/lv_obj.h +++ b/include/lvgl/core/lv_obj.h @@ -38,6 +38,9 @@ extern "C" { /********************** * TYPEDEFS **********************/ + +typedef void(*lv_delete_cb_t)(void * user_data); + /** * On/Off features controlling the object's behavior. * OR-ed values are possible @@ -352,6 +355,37 @@ bool lv_obj_is_valid(const lv_obj_t * obj); */ void lv_obj_null_on_delete(lv_obj_t ** obj_ptr); +/** + * Attach a delete callback to an object. + * + * The registered callback will be automatically invoked with `user_data` as + * its argument when the object is deleted, allowing associated resources to + * be released or any other cleanup logic to be executed by the callback. + * + * This is a utility function that simplifies attaching an `LV_EVENT_DELETE` + * event callback to `obj` and passing `user_data` to that callback when the + * object is deleted. + * + * The `lv_delete_dsc_t` returned by this function is automatically released when + * the object is deleted as well. + * + * @param obj Pointer to the LVGL object to attach the delete callback to. + * @param cb The delete callback function to register. + * @param user_data User data pointer passed to `cb` when the object is deleted. + * + * @return Pointer to the delete descriptor or NULL if the operation failed. + */ +lv_delete_dsc_t * lv_obj_add_delete_cb(lv_obj_t * obj, lv_delete_cb_t cb, void * user_data); + +/** + * Detach a delete callback from an object. + * + * Removes a delete descriptor previously created via @ref lv_obj_add_delete_cb + * + * @param dsc Pointer to the delete descriptor. Passing NULL results in a no-op + */ +void lv_obj_remove_delete_cb(lv_delete_dsc_t * dsc); + /** * Add an event handler to a widget that will load a screen on a trigger. * @param obj pointer to widget which should load the screen diff --git a/include/lvgl/lv_types.h b/include/lvgl/lv_types.h index 1c292e607b..aebba84ccd 100644 --- a/include/lvgl/lv_types.h +++ b/include/lvgl/lv_types.h @@ -112,6 +112,8 @@ typedef uint8_t lv_style_prop_t; typedef struct _lv_obj_class_t lv_obj_class_t; +typedef struct _lv_delete_dsc_t lv_delete_dsc_t; + typedef struct _lv_group_t lv_group_t; typedef struct _lv_display_t lv_display_t; diff --git a/src/core/lv_obj.c b/src/core/lv_obj.c index 1ccafa773e..8583c73122 100644 --- a/src/core/lv_obj.c +++ b/src/core/lv_obj.c @@ -45,6 +45,12 @@ typedef struct { bool reverse; } timeline_play_dsc_t; +struct _lv_delete_dsc_t { + lv_obj_t * obj; + lv_delete_cb_t cb; + void * user_data; +}; + /********************** * STATIC PROTOTYPES **********************/ @@ -63,6 +69,7 @@ static void screen_load_on_trigger_event_cb(lv_event_t * e); static void screen_create_on_trigger_event_cb(lv_event_t * e); static void play_timeline_on_trigger_event_cb(lv_event_t * e); static void delete_on_screen_unloaded_event_cb(lv_event_t * e); +static void call_delete_cb(lv_event_t * e); #if LV_USE_OBJ_PROPERTY static lv_result_t lv_obj_set_any(lv_obj_t *, lv_prop_id_t, const lv_property_t *); @@ -620,6 +627,41 @@ void * lv_obj_get_user_data(lv_obj_t * obj) return obj->user_data; } +lv_delete_dsc_t * lv_obj_add_delete_cb(lv_obj_t * obj, lv_delete_cb_t cb, void * user_data) +{ + LV_CHECK_ARG(obj != NULL, return NULL); + LV_CHECK_ARG(cb != NULL, return NULL); + + lv_delete_dsc_t * dsc = lv_malloc(sizeof(*dsc)); + if(!dsc) { + return NULL; + } + dsc->obj = obj; + dsc->cb = cb; + dsc->user_data = user_data; + + lv_event_dsc_t * event_dsc = lv_obj_add_event_cb(obj, call_delete_cb, LV_EVENT_DELETE, dsc); + if(!event_dsc) { + lv_free(dsc); + return NULL; + } + return dsc; +} + +void lv_obj_remove_delete_cb(lv_delete_dsc_t * dsc) +{ + if(!dsc) { + return; + } + + uint32_t count = lv_obj_remove_event_cb_with_user_data(dsc->obj, call_delete_cb, dsc); + if(count == 0) { + LV_LOG_WARN("Delete callback descriptor not found on object or already removed"); + return; + } + lv_free(dsc); +} + /********************** * STATIC FUNCTIONS **********************/ @@ -1344,6 +1386,19 @@ static void delete_on_screen_unloaded_event_cb(lv_event_t * e) lv_obj_delete(lv_event_get_target_obj(e)); } +static void call_delete_cb(lv_event_t * e) +{ + LV_ASSERT(e != NULL); + lv_obj_t * obj = lv_event_get_target_obj(e); + lv_delete_dsc_t * dsc = lv_event_get_user_data(e); + LV_ASSERT(dsc != NULL); + LV_ASSERT(dsc->cb != NULL); + LV_ASSERT(dsc->obj == obj); + + dsc->cb(dsc->user_data); + lv_obj_remove_delete_cb(dsc); +} + #if LV_USE_OBJ_PROPERTY static lv_point_t lv_obj_get_scroll_end_helper(lv_obj_t * obj) { diff --git a/tests/src/test_cases/widgets/test_obj_event.c b/tests/src/test_cases/widgets/test_obj_event.c new file mode 100644 index 0000000000..65fc91466e --- /dev/null +++ b/tests/src/test_cases/widgets/test_obj_event.c @@ -0,0 +1,71 @@ +#if LV_BUILD_TEST +#include "../lvgl.h" +#include "../../lvgl_private.h" + +#include "unity/unity.h" +static uint32_t cb_called_count; +static void * cb_item; + +static void my_delete_cb(void * item) +{ + cb_called_count ++; + cb_item = item; +} + +void setUp(void) +{ + cb_called_count = 0; + cb_item = NULL; +} + +void tearDown(void) +{ + lv_obj_clean(lv_screen_active()); +} + +void test_add_delete_cb_is_called_on_delete(void) +{ + lv_obj_t * obj = lv_obj_create(lv_screen_active()); + int item = 42; + + lv_delete_dsc_t * dsc = lv_obj_add_delete_cb(obj, my_delete_cb, &item); + TEST_ASSERT_NOT_NULL(dsc); + TEST_ASSERT_EQUAL(0, cb_called_count); + + lv_obj_delete(obj); + + TEST_ASSERT_EQUAL(1, cb_called_count); + TEST_ASSERT_EQUAL_PTR(&item, cb_item); +} + +void test_add_delete_cb_multiple_cbs_all_called(void) +{ + lv_obj_t * obj = lv_obj_create(lv_screen_active()); + int item = 0; + + lv_obj_add_delete_cb(obj, my_delete_cb, &item); + lv_obj_add_delete_cb(obj, my_delete_cb, &item); + lv_obj_add_delete_cb(obj, my_delete_cb, &item); + + lv_obj_delete(obj); + + TEST_ASSERT_EQUAL(3, cb_called_count); + TEST_ASSERT_EQUAL_PTR(&item, cb_item); +} +void test_add_delete_cb_can_be_removed(void) +{ + lv_obj_t * obj = lv_obj_create(lv_screen_active()); + int item = 42; + + lv_delete_dsc_t * dsc = lv_obj_add_delete_cb(obj, my_delete_cb, &item); + TEST_ASSERT_NOT_NULL(dsc); + TEST_ASSERT_EQUAL(0, cb_called_count); + lv_obj_remove_delete_cb(dsc); + + lv_obj_delete(obj); + + TEST_ASSERT_EQUAL(0, cb_called_count); + TEST_ASSERT_NULL(cb_item); +} + +#endif diff --git a/tests/src/test_cases/widgets/test_obj_event_invariants.c b/tests/src/test_cases/widgets/test_obj_event_invariants.c new file mode 100644 index 0000000000..cf67a09661 --- /dev/null +++ b/tests/src/test_cases/widgets/test_obj_event_invariants.c @@ -0,0 +1,26 @@ +#if LV_BUILD_TEST +#include "../lvgl.h" +#include "../../lvgl_private.h" +#include "unity/unity.h" +#include + +void setUp(void) +{ +} + +void tearDown(void) +{ + lv_obj_clean(lv_screen_active()); +} + +void test_delete_cb_null(void) +{ + lv_obj_t * obj = lv_obj_create(lv_screen_active()); + TEST_ASSERT_NULL(lv_obj_add_delete_cb(NULL, free, NULL)); + TEST_ASSERT_NULL(lv_obj_add_delete_cb(obj, NULL, NULL)); + + /*NULL is okay in remove functions*/ + lv_obj_remove_delete_cb(NULL); + TEST_PASS(); +} +#endif /*LV_BUILD_TEST*/