Files
imgui-godot/gdext/scripts/gds_bindings.py
2024-08-12 18:15:32 +02:00

637 lines
20 KiB
Python

import json
import os
import subprocess
# TODO: GodotType as class
# TODO: callbacks as Callable
type_map = {
"bool": "bool",
"bool*": "Array",
"char": "char",
"char*": "Array",
"const char*": "String",
"const float*": "Array",
"const ImGuiTableColumnSortSpecs*": "Array",
"const ImGuiWindowClass*": "Ref<ImGuiWindowClassPtr>",
"double": "double",
"double*": "Array",
"float": "real_t",
"float*": "Array",
"ImDrawList*": "Ref<ImDrawListPtr>",
"ImFont*": "int64_t",
"ImGuiID": "uint32_t",
"ImGuiIO*": "Ref<ImGuiIOPtr>",
"ImGuiListClipper*": "Ref<ImGuiListClipperPtr>",
"ImGuiSelectionUserData": "int64_t",
"ImGuiStyle*": "Ref<ImGuiStylePtr>",
"ImS16": "int16_t",
"ImS64": "int64_t",
"ImS8": "int8_t",
"ImTextureID": "Ref<Texture2D>",
"ImU16": "uint16_t",
"ImU32": "Color",
"ImU8": "uint8_t",
"ImVec2": "Vector2",
"ImVec4": "Color",
"ImVector_ImGuiSelectionRequest": "Array",
"int": "int",
"int*": "Array",
"short": "short",
"size_t": "int64_t",
"void": "void",
}
# use StringName for these const char* params
sn_names = (
"desc_id",
"id",
"label",
"name",
"str_id_begin",
"str_id_end",
"str_id",
"tab_or_docked_window_label",
)
exclude_funcs = (
"ImGui_ColorConvertFloat4ToU32",
"ImGui_ColorConvertHSVtoRGB",
"ImGui_ColorConvertU32ToFloat4",
"ImGui_CreateContext",
"ImGui_DestroyContext",
"ImGui_DestroyPlatformWindows",
"ImGui_EndFrame",
"ImGui_GetColorU32",
"ImGui_GetColorU32Ex",
"ImGui_GetColorU32ImU32",
"ImGui_GetColorU32ImU32Ex",
"ImGui_GetColorU32ImVec4",
"ImGui_GetCurrentContext",
"ImGui_GetIDStr",
"ImGui_GetKeyIndex",
"ImGui_NewFrame",
"ImGui_Render",
"ImGui_RenderPlatformWindowsDefault",
"ImGui_SetCurrentContext",
"ImGui_TextUnformatted", # this is called by Text()
"ImGui_TextUnformattedEx",
"ImGui_UpdatePlatformWindows",
)
include_structs = (
"ImGuiIO",
"ImGuiStyle",
"ImDrawList",
"ImGuiTableColumnSortSpecs",
"ImGuiTableSortSpecs",
# "ImGuiTextFilter",
"ImGuiWindowClass",
"ImGuiMultiSelectIO",
"ImGuiSelectionRequest",
# "ImGuiSelectionBasicStorage", # not useful
"ImGuiListClipper",
)
constructible_structs = ("ImGuiWindowClass", "ImGuiListClipper")
exclude_props = (
"MouseDown",
"KeysData",
"MouseClickedPos",
"MouseClickedTime",
"MouseClicked",
"MouseDoubleClicked",
"MouseClickedCount",
"MouseClickedLastCount",
"MouseReleased",
"MouseDownOwned",
"MouseDownOwnedUnlessPopupClose",
"MouseDownDuration",
"MouseDownDurationPrev",
"MouseDragMaxDistanceAbs",
"MouseDragMaxDistanceSqr",
"KeyMap",
"KeysDown",
"NavInputs",
)
array_types = {
"bool*": "bool",
"char*": "String",
"int*": "int",
"float*": "float",
"double*": "double",
"const float*": "float",
}
non_bitfield_enums = []
def is_obsolete(j):
for cond in j.get("conditionals", ()):
if cond["condition"] == "ifndef" and cond["expression"].find("OBSOLETE") != -1:
return True
return False
class Enum:
def __init__(self, j):
self.orig_name = j["name"]
self.obsolete = is_obsolete(j)
self.bitfield = self.orig_name.endswith("Flags_")
self.vals = []
self.name = self.orig_name.strip("_")
if self.name.startswith("ImGui"):
self.name = self.name.replace("ImGui", "", 1)
elif self.name.startswith("Im"):
self.name = self.name.replace("Im", "", 1)
ignore_endings = tuple(["COUNT", "_", "BEGIN", "END", "OFFSET", "SIZE"])
for e in j["elements"]:
name = e["name"]
if not is_obsolete(e) and not name.endswith(ignore_endings):
if name.startswith("ImGui"):
gdname = name.replace("ImGui", "", 1)
elif name.startswith("Im"):
gdname = name.replace("Im", "", 1)
self.vals.append((gdname, name))
def gen_def(self):
rv = f"enum {self.name} {{ "
for kv in self.vals:
rv += f"{kv[0]} = {kv[1]}, "
rv += "};"
return [rv]
def gen_cast(self):
macro = "VARIANT_BITFIELD_CAST" if self.bitfield else "VARIANT_ENUM_CAST"
return [f"{macro}(ImGui::Godot::ImGui::{self.name});"]
def gen_bindings(self):
macro = "BIND_BITFIELD_FLAG" if self.bitfield else "BIND_ENUM_CONSTANT"
rv = ""
for kv in self.vals:
rv += f"{macro}({kv[0]});"
return [rv]
def __str__(self):
return f"Enum {self.name} ({self.orig_name})"
class ReturnType:
def __init__(self, j):
self.orig_type = j["declaration"]
self.gdtype = type_map.get(self.orig_type)
self.is_struct = self.gdtype and self.gdtype.endswith("Ptr>")
if self.gdtype is None:
print(f"no type for rt {self.orig_type}")
class Param:
def __init__(self, j):
self.name = j["name"]
self.is_array = (
j["is_array"] or self.name == "values"
) # HACK: because is_array not set
self.is_varargs = j["is_varargs"]
if not self.is_varargs:
self.orig_type = j["type"]["declaration"]
else:
self.orig_type = None
self.gdtype = type_map.get(self.orig_type)
if self.is_array:
self.gdtype = "Array"
self.orig_type = self.orig_type[: self.orig_type.find("[")]
if self.gdtype == "String" and self.name in sn_names:
self.gdtype = "StringName"
if self.name == "items_separated_by_zeros":
self.gdtype = "Array"
dv = j.get("default_value")
if dv:
dv = dv.replace("ImVec2", "Vector2")
dv = dv.replace("ImVec4", "Color")
dv = dv.replace("FLT_MIN", "std::numeric_limits<float>::min()")
dv = dv.replace("FLT_MAX", "std::numeric_limits<float>::max()")
dv = dv.replace("sizeof", "(uint64_t)sizeof")
if self.gdtype is not None:
dv = dv.replace("NULL", f"{self.gdtype}()")
self.dv = dv
self.is_struct = self.gdtype and self.gdtype.endswith("Ptr>")
if self.gdtype is None:
print(f"no type for param {self.orig_type} {self.name}")
def gen_decl(self):
rv = f"{self.gdtype} {self.name}"
# if self.dv:
# rv += f" = {self.dv}"
return rv
def gen_def(self):
return f"{self.gdtype} {self.name}"
def gen_arg(self, safe_fmt=True):
if self.gdtype == "String":
if self.dv is not None:
return f"{self.name}.ptr() ? {self.name}.utf8().get_data() : nullptr"
else:
rv = f"{self.name}.utf8().get_data()"
if safe_fmt and self.name == "fmt":
return f'"%s", {rv}'
else:
return rv
elif self.gdtype == "Vector2":
return f"{{static_cast<float>({self.name}.x), static_cast<float>({self.name}.y)}}"
elif self.gdtype == "Ref<Texture2D>":
return f"(ImTextureID){self.name}->get_rid().get_id()"
elif self.gdtype == "Color":
if self.orig_type == "ImU32":
return f"{self.name}.to_abgr32()"
else:
return f"{{{self.name}.r, {self.name}.g, {self.name}.b, {self.name}.a}}"
elif self.gdtype == "StringName":
return f"sn_to_cstr({self.name})"
elif self.gdtype == "Array":
if self.is_array:
rv = f"({self.orig_type}*)GdsArray<{self.orig_type}>({self.name})"
elif self.name == "items_separated_by_zeros":
rv = f"({self.orig_type})GdsZeroArray({self.name})"
else:
atype = array_types[self.orig_type]
rv = f"{self.name}.size() == 0 ? nullptr : " if self.dv else ""
rv += f"({self.orig_type})GdsPtr<{atype}>({self.name}"
if atype == "String":
rv += ", buf_size, label"
rv += ")"
return rv
elif self.orig_type in ["ImFont*"]: # opaque pointers
return f"({self.orig_type}){self.name}"
elif self.is_struct:
return f"{self.name}->_GetPtr()"
else:
return self.name
class Function:
def __init__(self, j):
self.obsolete = is_obsolete(j)
self.orig_name = j["name"]
self.name = self.orig_name
if self.name.startswith("ImGui_"):
self.name = self.name.replace("ImGui_", "", 1)
self.rt = ReturnType(j["return_type"])
self.params = []
for ja in j["arguments"]:
self.params.append(Param(ja))
for p in self.params:
if p.is_varargs or p.orig_type == "va_list":
self.params.remove(p)
self.valid = (
self.rt.gdtype is not None
and not self.obsolete
and self.orig_name not in exclude_funcs
and not self.orig_name.endswith("V")
and not self.orig_name.startswith("ImGuiIO_")
and not self.orig_name.startswith("ImFont_")
# double underscore = internal
and not self.orig_name.startswith("ImDrawList__")
)
if self.valid:
for p in self.params:
if p.gdtype is None:
self.valid = False
if not self.valid and not self.orig_name in exclude_funcs and not self.obsolete:
print(f"skip function {self.orig_name}")
def gen_decl(self):
return [
f'static {self.rt.gdtype} {self.name}({", ".join(p.gen_decl() for p in self.params)});'
]
def gen_def(self):
fname = self.orig_name
safe_fmt = True
if fname == "ImGui_Text":
safe_fmt = False
fname = "ImGui_TextUnformatted" # do your own formatting
fcall = f'::{fname}({", ".join(p.gen_arg(safe_fmt) for p in self.params)})'
if self.rt.gdtype in ("Vector2", "Color"):
fcall = f"To{self.rt.gdtype}({fcall})"
elif self.rt.orig_type in ["ImFont*"]:
fcall = f"({self.rt.gdtype}){fcall}"
elif self.rt.gdtype in non_bitfield_enums:
fcall = f"static_cast<{self.rt.gdtype}>({fcall})"
rv = f'{self.rt.gdtype} ImGui::{self.name}({", ".join(p.gen_def() for p in self.params)}) {{'
if self.rt.is_struct:
rv += f"{self.rt.gdtype} rv;"
rv += "rv.instantiate();"
rv += f"rv->_SetPtr({fcall});"
rv += "return rv"
else:
if self.rt.gdtype != "void":
rv += "return "
rv += fcall
rv += "; }"
return [rv]
def gen_bindings(self):
rv = f'ClassDB::bind_static_method("ImGui", D_METHOD("{self.name}"'
for p in self.params:
rv += f', "{p.name}"'
rv += f"), &ImGui::{self.name}"
for p in self.params:
if p.dv is not None:
rv += f", DEFVAL({p.dv})"
rv += ");"
return [rv]
class Property:
variant_types = {
"float": "FLOAT",
"Vector2": "VECTOR2",
"bool": "BOOL",
}
array_types = {
"ImVec4": "PackedColorArray",
}
def __init__(self, j, name, struct_name):
self.struct_name = struct_name
self.name = name
self.is_array = j.get("is_array", False)
self.is_internal = j.get("is_internal", False)
self.orig_type = j["type"]["declaration"]
self.valid = False
if self.is_internal:
return
if self.name in exclude_props:
return
self.gdtype = type_map.get(self.orig_type, None)
if self.is_array:
self.array_type, self.array_size = self.orig_type[:-1].split("[")
self.gdtype = Property.array_types[self.array_type]
if self.gdtype == "String":
self.gdtype = None
try:
self.is_const = (
"const" in j["type"]["description"]["inner_type"]["storage_classes"]
)
except:
self.is_const = False
self.valid = self.gdtype is not None
if not self.valid:
print(f"skip property {self.orig_type} {self.struct_name}::{self.name}")
def gen_decl(self):
rv = f"{self.gdtype} _Get{self.name}();"
rv += f"void _Set{self.name}({self.gdtype} x);"
return rv
def gen_def(self):
# getter
rv = f"{self.gdtype} {self.struct_name}::_Get{self.name}() {{"
fcall = f"ptr->{self.name}"
# TODO: refactor
if self.orig_type == "ImVec2":
fcall = f"ToVector2({fcall})"
elif self.orig_type in ["ImFont*"]:
fcall = f"(int64_t){fcall}"
elif self.gdtype == "PackedColorArray":
fcall = f"ToPackedColorArray({fcall}, {self.array_size})"
dv = "{}"
if self.gdtype.startswith("BitField"):
dv = "0"
elif self.gdtype in non_bitfield_enums:
fcall = f"static_cast<{self.gdtype}>({fcall})"
elif self.orig_type.startswith("ImVector_"):
fcall = f"FromImVector({fcall})"
elif self.orig_type == "const ImGuiTableColumnSortSpecs*":
fcall = f"SpecsToArray(ptr)"
rv += f"if (ptr) [[likely]] return {fcall}; else return {dv};"
rv += "}"
# setter
rv += f"void {self.struct_name}::_Set{self.name}({self.gdtype} x) {{"
if self.is_const:
rv += f'ERR_FAIL_MSG("{self.name} is const");'
else:
x = "x"
if self.orig_type == "ImVec2":
x = "{static_cast<float>(x.x), static_cast<float>(x.y)}"
elif self.orig_type in ["ImFont*"]:
x = "(ImFont*)x"
elif self.gdtype in non_bitfield_enums:
x = f"static_cast<{self.orig_type}>(x)"
elif self.orig_type.startswith("ImVector_"):
x = "ToImVector(x)"
if self.gdtype == "PackedColorArray":
rv += f"FromPackedColorArray(x, ptr->{self.name});"
else:
rv += f"ptr->{self.name} = {x};"
rv += "}"
return rv
def gen_bindings(self):
getter = f"_Get{self.name}"
setter = f"_Set{self.name}"
rv = f'ClassDB::bind_method(D_METHOD("{getter}"), &{self.struct_name}::{getter});'
rv += f'ClassDB::bind_method(D_METHOD("{setter}", "x"), &{self.struct_name}::{setter});'
vtype = Property.variant_types.get(self.gdtype, "INT")
rv += f'ADD_PROPERTY(PropertyInfo(Variant::{vtype}, "{self.name}"), "{setter}", "{getter}");'
return rv
class Struct:
def __init__(self, j):
self.orig_name = j["name"]
self.name = self.orig_name + "Ptr"
self.valid = self.orig_name in include_structs
self.constructible = self.orig_name in constructible_structs
self.properties = []
if not self.valid:
return
for jfield in j["fields"]:
prop = Property(jfield, jfield["name"], self.name)
if prop.valid:
self.properties.append(prop)
def gen_decl(self):
rv = f"class {self.name} : public RefCounted {{"
rv += f"GDCLASS({self.name}, RefCounted);"
rv += "protected: static void _bind_methods();"
rv += "public:"
# needs constructor for non-zero values
if self.orig_name == "ImGuiWindowClass":
rv += "ImGuiWindowClassPtr() { ptr->ParentViewportId = -1; ptr->DockingAllowUnclassed = true; }"
if not self.constructible:
rv += f"void _SetPtr({self.orig_name}* p) {{ ptr = p; }}"
rv += f"{self.orig_name}* _GetPtr() {{ return ptr; }}"
else:
rv += f"{self.orig_name}* _GetPtr() {{ return ptr.get(); }}"
for prop in self.properties:
rv += prop.gen_decl()
rv += "private:"
if not self.constructible:
rv += f"{self.orig_name}* ptr = nullptr;"
else:
rv += f"std::unique_ptr<{self.orig_name}> ptr = std::make_unique<{self.orig_name}>();"
rv += "};"
return [rv]
def gen_def(self):
rv = f"void {self.name}::_bind_methods() {{"
for prop in self.properties:
rv += prop.gen_bindings()
rv += "}"
for prop in self.properties:
rv += prop.gen_def()
return [rv]
def gen_bindings(self):
return [f"ClassDB::register_class<{self.name}>();"]
class JsonParser:
array_types = {
"bool*": "bool",
"char*": "String",
"int*": "int",
"float*": "float",
"double*": "double",
}
def __init__(self):
for s in include_structs:
type_map[f"{s}*"] = f"Ref<{s}Ptr>"
self.enums = []
self.structs = []
self.funcs = []
self.enum_defs = []
self.enum_casts = []
self.enum_binds = []
self.func_decls = []
self.func_binds = []
self.func_defs = []
self.struct_decls = []
self.struct_decls_fwd = []
self.struct_defs = []
self.struct_binds = []
def write(self):
try:
os.mkdir("gen")
except:
pass
with open("gen/imgui_bindings.gen.h", "w") as fi:
out = (
["namespace ImGui::Godot {"]
+ self.struct_decls_fwd
+ [
"class ImGui : public Object {",
" GDCLASS(ImGui, Object);",
"protected:",
" static void _bind_methods();" "public:",
]
+ self.enum_defs
+ self.func_decls
+ ["};"]
+ self.struct_decls
+ ["} // namespace ImGui::Godot"]
+ self.enum_casts
)
fi.write("\n".join(out))
with open("gen/imgui_bindings_impl.gen.h", "w") as fi:
out = (
["void ImGui::_bind_methods() {"]
+ self.enum_binds
+ self.struct_binds
+ self.func_binds
+ ["}"]
+ self.struct_defs
+ self.func_defs
)
fi.write("\n".join(out))
def load(self, jdat):
enums = []
for je in jdat["enums"]:
e = Enum(je)
self.enum_defs += e.gen_def()
self.enum_casts += e.gen_cast()
self.enum_binds += e.gen_bindings()
enums.append(e)
if e.bitfield:
type_map[e.orig_name.strip("_")] = f"BitField<ImGui::{e.name}>"
else:
type_map[e.orig_name.strip("_")] = f"ImGui::{e.name}"
non_bitfield_enums.append(f"ImGui::{e.name}")
for js in jdat["structs"]:
s = Struct(js)
if s.valid:
self.structs.append(s)
for s in self.structs:
self.struct_decls_fwd += [f"class {s.name};"]
self.struct_decls += s.gen_decl()
self.struct_defs += s.gen_def()
self.struct_binds += s.gen_bindings()
for jf in jdat["functions"]:
f = Function(jf)
if f.valid:
self.funcs.append(f)
for f in self.funcs:
self.func_decls += f.gen_decl()
self.func_defs += f.gen_def()
self.func_binds += f.gen_bindings()
def main():
os.makedirs("gen", exist_ok=True)
subprocess.call(
"python dear_bindings/dear_bindings.py -o gen/cimgui imgui/imgui.h", shell=True
)
parser = JsonParser()
with open("gen/cimgui.json") as jfi:
jdat = json.loads(jfi.read())
parser.load(jdat)
parser.write()
subprocess.call("clang-format -i gen/imgui_bindings*", shell=True)
if __name__ == "__main__":
main()