LVGL Event Bubbling: Route Composite-Widget Actions Without Double Handling

A composite control needs one command owner even when several callbacks observe its click. Use the original event target to identify the source, the current target to identify the receiving object, and an explicit routing policy to decide who may issue the command. Bubbling distributes an event; it does not deduplicate application work.

Applicability: this procedure is pinned to LVGL v9.3.0 object events. It covers ordinary click propagation through a fixed object hierarchy, without deletion, reparenting, preprocessing callbacks, gestures or scrolling during dispatch. The proposed fixture has not been executed. Check your exact release before extending these conclusions to other event codes or custom widget classes.

Three-level click route with stable original target and changing current target, followed by two different stopping policies.
Hypothetical routing fixture. The arrows are event propagation, not multiple user actions; no LVGL execution is claimed.

1. Draw the route before adding another handler

Consider a hypothetical setpoint row containing a minus button, a value label and a plus button. The row belongs to a panel. The plus button is the click source; the row owns the increment command; the panel records telemetry only. Decorative labels do not independently issue commands. This is an application policy, not a default LVGL behavior.

Make a routing inventory containing each object’s stable identity, parent, relevant flags, registered event filters and permitted business effects. Include every callback registration, because registering the same function twice can produce duplicate work even without an ancestor. Keep a separate counter for increment commands rather than treating the number of callback invocations as the acceptance metric.

The 9.3 event guide distinguishes the originating target from the object currently receiving the event. For this widget-only fixture, the object-specific accessors are convenient. At the plus callback both identities are plus. At the row callback the original target remains plus while the current target is row. At the panel callback they are plus and panel.

2. Verify every propagation hop

In the v9.3.0 object dispatch implementation, ordinary bubbling tests the flag on the current object before forwarding to its parent. Therefore enabling LV_OBJ_FLAG_EVENT_BUBBLE on plus allows the click to reach row. Continuing from row to panel also requires the flag on row. A flag on panel alone cannot pull a child’s click through a missing intermediate hop.

Plus flag Row flag Expected receivers for plus click
Off On Plus only
On Off Plus, then row
On On Plus, row, then panel

Keep the panel’s onward route outside the fixture or explicitly terminate it. Otherwise a screen-level callback may be another observer. Do not generalize this table to every event: the pinned source has event-code exceptions, including child-created/deleted notifications and non-bubbling drawing or geometry-related events. Establish the table specifically for LV_EVENT_CLICKED.

3. Interpret source identity separately from receiver context

The row callback should accept only the click sources it understands. A plus identity maps to increment, minus maps to decrement, and an unrelated nested control maps to no row action. A click directly addressed to row has row as its original target and needs its own explicit policy. Comparing only current target to row proves where the callback runs, not which child requested the action.

Associate a stable action identifier with each accepted source or maintain a controlled source-to-action map. Do not derive business identity from label text, child index or a translated caption. Those presentation details can change while the same command remains valid. If the composite contains another independently actionable control, its command must not become an accidental row increment.

Also distinguish callback context from source context. The generic event dispatcher installs the user-data value belonging to each callback descriptor before invoking it. The row callback’s user data is therefore its registered receiver context; it is not automatically the plus button’s data. Resolve the original source deliberately and keep callback context alive for the registered lifetime.

4. Choose the narrow stopping mechanism

The event API exposes separate stop-bubbling and stop-processing operations. Their difference matters when the row has both a command callback and an audit callback. In this pinned dispatch path, stopping bubbling prevents forwarding to ancestors while leaving later matching callbacks on the current object available. Stopping processing ends further processing of that event, including remaining callbacks and subsequent propagation.

Use the first when the row should consume the upward route but still run its local observers. Use the second only when skipping the remaining event processing is intentional. Neither operation rolls back a command already emitted by an earlier callback. A child that increments before forwarding cannot be repaired by stopping propagation at the parent.

Prefer a clear ownership rule over relying on callback order for correctness. In the sample policy, plus never increments directly. The row increments once after validating the source. Panel telemetry never increments. A local audit callback can run before or after the command without changing the number of commands.

5. Build a duplicate-command regression fixture

  1. Register two ordinary row callbacks: the command owner and a passive audit observer. Add passive observers at plus and panel. Give all registrations stable names.
  2. Send a synthetic click to plus to test dispatch alone. Record event code, original target, current target, registration identity and command count.
  3. Run all three flag combinations in the routing table. Assert receiver membership and one increment only when the row receives the eligible click.
  4. Enable stop-bubbling in the row owner. Expect the local audit to remain observable and panel to disappear from the route.
  5. In a separate case, enable stop-processing in that callback. Expect later ordinary row observers and panel to be skipped; do not claim earlier callbacks are undone.
  6. Send clicks to minus, row itself and an unrelated child. Verify their distinct action mapping. Intentionally duplicate a registration to prove the command counter can detect the defect.

Then repeat through the actual input path. Synthetic dispatch does not prove hit testing, click recognition or input-device timing. Save the hierarchy, flags, registration order, LVGL tag and expected trace with the fixture. The useful release artifact is a reproducible routing contract: several observers may see one interaction, but only its designated owner may turn it into a business command.

Similar Posts