Created
May 11, 2025 20:08
-
-
Save lassade/5182f416c8c8d0b08b3e7c5b2bec7565 to your computer and use it in GitHub Desktop.
A simple example of how a recursive inspector can be implemented
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| pub fn inspector(context: anytype) void { | |
| if (imgui.Begin("Inspector", .{})) { | |
| const World = @typeInfo(@TypeOf(context.world)).pointer.child; | |
| inline for (World.entities, 0..) |E, k| { | |
| if (k == context.target.kind) { | |
| const slice = context.world.tables[k].columns.slice(); | |
| if (context.target.id >= slice.len) break; // invalid | |
| const ver = slice.items(._ver).ptr[context.target.id]; | |
| if (ver != context.target.ver) break; // killed | |
| const tags = &slice.items(._tags).ptr[context.target.id]; | |
| if (!tags.alive) break; // dead | |
| inspect(core.Tags, "tags", tags, context); | |
| // editing side effects: like moved flag and undo info | |
| inline for (@typeInfo(E).@"struct".fields, 0..) |field, c| { | |
| const C = field.type; | |
| // use the hiereachy window for these components | |
| if (C == core.Parent) continue; | |
| if (C == core.Children) continue; | |
| // inspect component | |
| const data = &slice.items(@enumFromInt(c)).ptr[context.target.id]; | |
| inspect(C, field.name, data, context); | |
| } | |
| // // apply side effects if any | |
| // tags.* = tags.merge(context.side_effects); | |
| // context.side_effects = .{}; | |
| break; | |
| } | |
| } | |
| imgui.End(); | |
| } | |
| } | |
| pub fn inspect(comptime T: type, label: [:0]const u8, data: *T, context: anytype) void { | |
| if (comptime std.meta.hasMethod(T, "inspect")) { | |
| data.inspect(label, context); // custom inspector | |
| return; | |
| } | |
| if (T == Allocator) { | |
| return; | |
| } else { | |
| // by default it will ignore unsupported types | |
| switch (@typeInfo(T)) { | |
| .bool => |_| { | |
| _ = imgui.Checkbox(label, data); | |
| return; | |
| }, | |
| .int => |i| { | |
| // int conversion should be very cheaper | |
| if (i.signedness == .signed) { | |
| if (i.bits <= 64) { | |
| const min: u64 = std.math.minInt(T); | |
| const max: u64 = std.math.maxInt(T); | |
| var temp: i64 = data.*; | |
| if (imgui.DragScalar(label, .S64, &temp, .{ .p_min = &min, .p_max = &max })) { | |
| data.* = @truncate(temp); | |
| } | |
| return; | |
| } | |
| } else { | |
| if (i.bits <= 64) { | |
| const min: u64 = 0; | |
| const max: u64 = std.math.maxInt(T); | |
| var temp: u64 = data.*; | |
| if (imgui.DragScalar(label, .U64, &temp, .{ .p_min = &min, .p_max = &max })) { | |
| data.* = @truncate(temp); | |
| } | |
| return; | |
| } | |
| } | |
| }, | |
| .float => |f| { | |
| switch (f.bits) { | |
| 32 => _ = imgui.DragScalar(label, .Float, data, .{}), | |
| 64 => _ = imgui.DragScalar(label, .Double, data, .{}), | |
| else => { | |
| // heavy path | |
| var temp: f64 = @floatCast(data.*); | |
| if (imgui.DragScalar(label, .Double, &temp, .{})) { | |
| data.* = @floatCast(temp); | |
| } | |
| }, | |
| } | |
| return; | |
| }, | |
| .pointer => |p| { | |
| imgui.BeginDisabled(.{ .disabled = p.is_const }); | |
| defer imgui.EndDisabled(); | |
| switch (p.size) { | |
| .One => { | |
| inspect(p.child, label, @constCast(data.*), context); | |
| return; | |
| }, | |
| .Slice => { | |
| // todo: handle text | |
| if (imgui.TreeNode(label)) { | |
| defer imgui.TreePop(); | |
| for (0..data.len) |i| { | |
| const scratch = context.scratch; | |
| defer context.scratch = scratch; // revert | |
| const element_label = std.fmt.bufPrintZ(scratch, "[{d}]", .{i}) catch return; | |
| context.scratch = scratch[element_label.len + 1 ..]; // bump scratch space | |
| inspect(p.child, element_label, &data.*[i], context); | |
| } | |
| } | |
| return; | |
| }, | |
| else => {}, | |
| } | |
| }, | |
| // todo: .optional => |o| { | |
| // if (data.*) |*value| { | |
| // inspect(o.child, label, value, context); | |
| // } else { | |
| // _ = imgui.InputText(label, "null", 4, .{ .flags = .{ .ReadOnly = true } }); | |
| // } | |
| // return; | |
| // }, | |
| .vector => |v| { | |
| if (v.len <= 16) scalar: { | |
| const data_type: imgui.ImGuiDataType = switch (v.child) { | |
| // bool => .bool, | |
| i8 => .S8, | |
| i16 => .S16, | |
| i32 => .S32, | |
| i64 => .S64, | |
| u8 => .U8, | |
| u16 => .U16, | |
| u32 => .U32, | |
| u64 => .U64, | |
| f32 => .Float, | |
| f64 => .Double, | |
| else => break :scalar, | |
| }; | |
| _ = imgui.DragScalarN(label, data_type, data, v.len, .{}); | |
| return; | |
| } | |
| // array fallback | |
| inspect([v.len]v.child, label, @ptrCast(data), context); | |
| return; | |
| }, | |
| .array => |a| { | |
| if (imgui.TreeNode(label)) { | |
| defer imgui.TreePop(); | |
| for (0..a.len) |i| { | |
| const scratch = context.scratch; | |
| defer context.scratch = scratch; // revert | |
| const element_label = std.fmt.bufPrintZ(scratch, "[{d}]", .{i}) catch return; | |
| context.scratch = scratch[element_label.len + 1 ..]; // bump scratch space | |
| inspect(a.child, element_label, &data[i], context); | |
| } | |
| } | |
| return; | |
| }, | |
| .@"struct" => |s| { | |
| if (imgui.TreeNode(label)) { | |
| defer imgui.TreePop(); | |
| if (s.layout == .@"packed") { | |
| // packed use temp value | |
| inline for (s.fields) |field| { | |
| var temp: field.type = @field(data, field.name); | |
| inspect(field.type, field.name, &temp, context); | |
| @field(data, field.name) = temp; | |
| } | |
| } else { | |
| inline for (s.fields) |field| { | |
| inspect(field.type, field.name, &@field(data, field.name), context); | |
| } | |
| } | |
| } | |
| return; | |
| }, | |
| .@"union" => |u| { | |
| if (imgui.TreeNode(label)) { | |
| defer imgui.TreePop(); | |
| if (u.tag_type) |UnionTagType| { | |
| var tag: UnionTagType = data.*; | |
| inspect(UnionTagType, "tag", &tag, context); | |
| inline for (u.fields) |field| { | |
| if (data.* == @field(UnionTagType, field.name)) { | |
| inspect(field.type, "value", &@field(data, field.name), context); | |
| break; | |
| } | |
| } | |
| } else { | |
| // todo: ??? | |
| } | |
| } | |
| return; | |
| }, | |
| .@"enum" => |e| { | |
| const variants = comptime blk: { | |
| var arr: [e.fields.len][*:0]const u8 = undefined; | |
| for (e.fields, 0..) |field, i| arr[i] = field.name; | |
| const out = arr; | |
| break :blk out; | |
| }; | |
| var temp: c_int = @intCast(@intFromEnum(data.*)); | |
| if (imgui.Combo(label, &temp, &variants, .{})) { | |
| data.* = @enumFromInt(temp); | |
| } | |
| return; | |
| }, | |
| else => {}, | |
| } | |
| imgui.TextUnformatted(label); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment