@@ -3,6 +3,9 @@ const lvgl = @import("lvgl.zig");
33
44const logger = std .log .scoped (.ui );
55
6+ // NOTE: ui.init() must set this (e.g. widget.allocator = opt.allocator).
7+ pub var allocator : std.mem.Allocator = undefined ;
8+
69// defined in ui.c
710extern fn nm_keyboard_popon (input : * lvgl.LvObj ) void ;
811extern fn nm_keyboard_popoff () void ;
@@ -60,18 +63,34 @@ pub fn topdrop(onoff: enum { show, remove }) void {
6063/// provided as btns arg to modal.
6164pub const ModalButtonCallbackFn = * const fn (index : usize ) void ;
6265
66+ /// Context stored in the modal window's user data.
67+ /// We intentionally store a *data pointer* in LVGL user_data (void*), never a function
68+ /// pointer, because data-pointer <-> function-pointer casts are not portable and can
69+ /// break across Zig versions / ABIs.
70+ ///
71+ /// We also store the allocator used to allocate this context so the delete handler
72+ /// can always free with the same allocator.
73+ const ModalCtx = struct {
74+ alloc : std.mem.Allocator ,
75+ cb : ModalButtonCallbackFn ,
76+ };
77+
6378/// shows a non-dismissible window using the whole screen real estate;
6479/// for use in place of lv_msgbox_create.
6580///
6681/// while all heap-alloc'ed resources are free'd automatically right before cb is called,
6782/// the value of title, text and btns args must live at least as long as cb; they are
6883/// memory-managed by the callers.
69- ///
70- /// note: the cb callback must have @alignOf(ModalbuttonCallbackFn) alignment.
7184pub fn modal (title : [* :0 ]const u8 , text : [* :0 ]const u8 , btns : []const [* :0 ]const u8 , cb : ModalButtonCallbackFn ) ! void {
7285 const win = try lvgl .Window .newTop (60 , title );
7386 errdefer win .destroy (); // also deletes all children created below
74- win .setUserdata (cb );
87+
88+ const ctx = try allocator .create (ModalCtx );
89+ ctx .* = .{ .alloc = allocator , .cb = cb };
90+ win .setUserdata (ctx );
91+
92+ // Free context when the window is deleted, regardless of deletion path.
93+ _ = win .on (.delete , nm_modal_delete_callback , null );
7594
7695 const wincont = win .content ().flex (.column , .{ .cross = .center , .track = .center });
7796 const msg = try lvgl .Label .new (wincont , text , .{ .pos = .center });
@@ -89,7 +108,10 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
89108 const btn = try lvgl .TextButton .new (btncont , btext );
90109 btn .setFlag (.event_bubble );
91110 btn .setFlag (.user1 ); // .user1 indicates actionable button in callback
92- btn .setUserdata (@ptrFromInt (i )); // button index in callback
111+
112+ // Store (i+1) so userdata is never null (0). In callback: idx = intFromPtr - 1.
113+ btn .setUserdata (@ptrFromInt (i + 1 ));
114+
93115 btn .setWidth (btnwidth );
94116 if (i == 0 ) {
95117 btn .addStyle (lvgl .nm_style_btn_red (), .{});
@@ -98,17 +120,38 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
98120 _ = btncont .on (.click , nm_modal_callback , win .lvobj );
99121}
100122
123+ /// Frees modal context when the window is deleted. This runs for all deletion paths
124+ /// (button click, screen unload, parent cleanup, etc.) and avoids leaks/double-frees.
125+ export fn nm_modal_delete_callback (e : * lvgl.LvEvent ) callconv (.C ) void {
126+ const win = lvgl.Window { .lvobj = e .target () };
127+
128+ const ctx_any = win .userdata () orelse return ;
129+ const ctx : * ModalCtx = @ptrCast (@alignCast (ctx_any ));
130+
131+ // Clear userdata so any accidental future access becomes a null-check failure.
132+ win .setUserdata (null );
133+
134+ ctx .alloc .destroy (ctx );
135+ }
136+
101137export fn nm_modal_callback (e : * lvgl.LvEvent ) callconv (.C ) void {
102- if (e .userdata ()) | edata | {
103- const target = lvgl.Container { .lvobj = e .target () }; // type doesn't really matter
104- if (! target .hasFlag (.user1 )) { // .user1 is set in modal setup
105- return ;
106- }
138+ const edata = e .userdata () orelse return ;
107139
108- const btn_index = @intFromPtr (target .userdata ());
109- const win = lvgl.Window { .lvobj = @ptrCast (edata ) };
110- const cb : ModalButtonCallbackFn = @alignCast (@ptrCast (win .userdata ()));
111- win .destroy ();
112- cb (btn_index );
140+ const target = lvgl.Container { .lvobj = e .target () }; // type doesn't really matter
141+ if (! target .hasFlag (.user1 )) { // .user1 is set in modal setup
142+ return ;
113143 }
144+
145+ const idx_any = target .userdata () orelse return ;
146+ const btn_index : usize = @intFromPtr (idx_any ) - 1 ;
147+
148+ const win = lvgl.Window { .lvobj = @ptrCast (edata ) };
149+ const ctx_any = win .userdata () orelse return ;
150+ const ctx : * ModalCtx = @ptrCast (@alignCast (ctx_any ));
151+ const cb = ctx .cb ;
152+
153+ // Destroying the window triggers LV_EVENT_DELETE, which frees ctx.
154+ win .destroy ();
155+
156+ cb (btn_index );
114157}
0 commit comments