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*/