@@ -32,13 +32,29 @@ pub fn draw(_: ?*anyopaque) anyerror!void {
3232 var hbox = dvui .box (@src (), .{ .dir = .horizontal }, .{ .expand = .both });
3333 defer hbox .deinit ();
3434
35- OutputLog .lock ();
36- defer OutputLog .unlock ();
37- const lines = OutputLog .items ();
35+ // Snapshot into the frame arena rather than holding `OutputLog`'s lock across the draw
36+ // below: that draw resolves the theme's mono font (see `mono` below), and some themes'
37+ // fonts fail to resolve, which logs a warning — reentering `OutputLog.append` on this
38+ // same thread. Holding the lock that long turns that into a self-deadlock (the log's
39+ // spinlock is not reentrant), so we copy what we need and unlock before drawing anything.
40+ const arena = dvui .currentWindow ().arena ();
41+ const lines : []const OutputLog.Line = blk : {
42+ OutputLog .lock ();
43+ defer OutputLog .unlock ();
44+ const src = OutputLog .items ();
45+ const copy = arena .alloc (OutputLog .Line , src .len ) catch break :blk &.{};
46+ for (src , copy ) | s , * d | {
47+ d .* = .{
48+ .level = s .level ,
49+ .scope = arena .dupe (u8 , s .scope ) catch "" ,
50+ .text = arena .dupe (u8 , s .text ) catch "" ,
51+ };
52+ }
53+ break :blk copy ;
54+ };
3855
3956 // Distinct scopes seen so far, in first-seen order — small (one per active plugin), so a
40- // linear scan per line is cheap. Arena-backed: pure per-frame scratch for the tab strip.
41- const arena = dvui .currentWindow ().arena ();
57+ // linear scan per line is cheap.
4258 var scopes : std .ArrayListUnmanaged ([]const u8 ) = .empty ;
4359 for (lines ) | line | {
4460 var seen = false ;
0 commit comments