Skip to content

Instantly share code, notes, and snippets.

@lassade
Created May 11, 2025 20:08
Show Gist options
  • Select an option

  • Save lassade/5182f416c8c8d0b08b3e7c5b2bec7565 to your computer and use it in GitHub Desktop.

Select an option

Save lassade/5182f416c8c8d0b08b3e7c5b2bec7565 to your computer and use it in GitHub Desktop.
A simple example of how a recursive inspector can be implemented
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