From 1d7de4f91dda6fd8afeb8d1279db63195405dd45 Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Sun, 1 Feb 2026 22:22:31 -0700 Subject: [PATCH 1/9] imgui combo binding --- python/taichi/ui/imgui.py | 26 +++++++++++++ taichi/python/export_ggui.cpp | 73 +++++++++++++++++++++++++++++++---- taichi/ui/common/gui_base.h | 4 ++ taichi/ui/ggui/gui.cpp | 11 ++++++ taichi/ui/ggui/gui.h | 3 ++ taichi/ui/ggui/gui_metal.h | 3 ++ taichi/ui/ggui/gui_metal.mm | 7 ++++ 7 files changed, 120 insertions(+), 7 deletions(-) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 570a815526001..8bb057288a653 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -116,3 +116,29 @@ def button(self, text): text (str): a line of text to be shown next to the button. """ return self.gui.button(text) + + def combo(self, label, current_index, items): + """Combo box (dropdown) for selecting from a tuple of items. + + Args: + label (str): Label for the combo box. + current_index (int): Currently selected index (0-based). + items (tuple[str, ...]): Tuple of string options. + + Returns: + int: The newly selected index. + + Note: + Converting Python strings to ImGui format is cached when the same + tuple object is passed across frames. For best performance, define + options at module or class level (not inside the render loop):: + + DIFFICULTIES = ("Easy", "Medium", "Hard") # module level + + while window.running: + with gui.sub_window(...) as w: + # Cached - same tuple object each frame + idx = w.combo("Difficulty", idx, DIFFICULTIES) + window.show() + """ + return self.gui.combo(label, current_index, items) diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 965c54de70b01..69a9b942c7424 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -1,4 +1,5 @@ +#include #include #include "pybind11/pybind11.h" #include @@ -59,9 +60,27 @@ py::array_t mat4_to_nparray(glm::mat4 mat) { } struct PyGui { - GuiBase *gui; // not owned - void begin(std::string name, float x, float y, float width, float height) { - gui->begin(name, x, y, width, height); + GuiBase *gui = nullptr; // not owned + + // Cache for combo items: label -> (tuple identity, strings, cstr_ptrs) + // Frame-based cleanup removes entries not used since last frame + struct ComboCache { + py::tuple items_tuple; // for identity comparison + std::vector items_str; // owns the string data + std::vector items_cstr; // points into items_str + bool touched = false; // used this frame? + }; + std::unordered_map combo_cache_; + + void begin(std::string name, + float x, + float y, + float width, + float height, + bool movable = true, + bool resizable = true, + bool collapsible = true) { + gui->begin(name, x, y, width, height, movable, resizable, collapsible); } void end() { gui->end(); @@ -92,6 +111,38 @@ struct PyGui { bool button(std::string name) { return gui->button(name); } + int combo(std::string label, int current_item, py::tuple items_py) { + auto it = combo_cache_.find(label); + + // Cache hit if same label and same tuple identity + if (it == combo_cache_.end() || !it->second.items_tuple.is(items_py)) { + // Build new cache entry + ComboCache cache; + cache.items_tuple = items_py; + for (auto item : items_py) { + cache.items_str.push_back(item.cast()); + } + for (const auto &s : cache.items_str) { + cache.items_cstr.push_back(s.c_str()); + } + combo_cache_[label] = std::move(cache); + it = combo_cache_.find(label); + } + it->second.touched = true; + return gui->combo(label, current_item, it->second.items_cstr); + } + + // Called at frame end to clean up stale cache entries + void frame_end() { + for (auto it = combo_cache_.begin(); it != combo_cache_.end();) { + if (!it->second.touched) { + it = combo_cache_.erase(it); + } else { + it->second.touched = false; + ++it; + } + } + } }; struct PyCamera { @@ -511,6 +562,7 @@ struct PyCanvas { struct PyWindow { std::unique_ptr window{nullptr}; + std::unique_ptr py_gui_{nullptr}; PyWindow(Program *prog, std::string name, @@ -596,6 +648,9 @@ struct PyWindow { void show() { window->show(); + if (py_gui_) { + py_gui_->frame_end(); + } } bool is_pressed(std::string button) { @@ -635,9 +690,12 @@ struct PyWindow { return scene; } - PyGui gui() { - PyGui gui = {window->gui()}; - return gui; + PyGui &gui() { + if (!py_gui_) { + py_gui_ = std::make_unique(); + py_gui_->gui = window->gui(); + } + return *py_gui_; } // this is so that the GUI class does not need to use any pybind related stuff @@ -699,7 +757,8 @@ void export_ggui(py::module &m) { .def("slider_int", &PyGui::slider_int) .def("slider_float", &PyGui::slider_float) .def("color_edit_3", &PyGui::color_edit_3) - .def("button", &PyGui::button); + .def("button", &PyGui::button) + .def("combo", &PyGui::combo); py::class_(m, "PyScene") .def(py::init<>()) diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index a7d296cbe7b06..a6ff5ab0eae14 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "taichi/ui/utils/utils.h" namespace taichi::ui { @@ -26,6 +27,9 @@ class GuiBase { virtual glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) = 0; virtual bool button(const std::string &text) = 0; + virtual int combo(const std::string &label, + int current_item, + const std::vector &items) = 0; virtual void prepare_for_next_frame() = 0; virtual ~GuiBase() = default; }; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index f5bc10833880b..0e94d10664d83 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -214,6 +214,17 @@ bool Gui::button(const std::string &text) { return ImGui::Button(text.c_str()); } +int Gui::combo(const std::string &label, + int current_item, + const std::vector &items) { + if (!initialized()) { + return current_item; + } + ImGui::Combo(label.c_str(), ¤t_item, items.data(), + static_cast(items.size())); + return current_item; +} + void Gui::draw(taichi::lang::CommandList *cmd_list) { // Rendering ImGui::Render(); diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index e54314e570f7a..85e6b5916039b 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -49,6 +49,9 @@ class TI_DLL_EXPORT Gui final : public GuiBase { // TODO: consider renaming this? glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) override; bool button(const std::string &text) override; + int combo(const std::string &label, + int current_item, + const std::vector &items) override; void draw(taichi::lang::CommandList *cmd_list); diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index 039c87fe9bc3a..23844342fba05 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -43,6 +43,9 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { // TODO: consider renaming this? glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) override; bool button(const std::string &text) override; + int combo(const std::string &label, + int current_item, + const std::vector &items) override; void prepare_for_next_frame() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index 61cbfb1b7c5b5..ea89700416e12 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -91,6 +91,13 @@ return ImGui::Button(text.c_str()); } +int GuiMetal::combo(const std::string &label, int current_item, + const std::vector &items) { + ImGui::Combo(label.c_str(), ¤t_item, items.data(), + static_cast(items.size())); + return current_item; +} + void GuiMetal::draw(taichi::lang::CommandList *cmd_list) { ImGui_ImplMetal_NewFrame(current_rpd_); From 7630755706c953c9c0b82fd8fbf28a933b685565 Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Sun, 1 Feb 2026 22:52:26 -0700 Subject: [PATCH 2/9] input/drag int/float imgui bindings --- python/taichi/ui/imgui.py | 54 +++++++++++++++++++++++++++++++++++ taichi/python/export_ggui.cpp | 24 ++++++++++++++++ taichi/ui/common/gui_base.h | 12 ++++++++ taichi/ui/ggui/gui.cpp | 40 ++++++++++++++++++++++++++ taichi/ui/ggui/gui.h | 12 ++++++++ taichi/ui/ggui/gui_metal.h | 12 ++++++++ taichi/ui/ggui/gui_metal.mm | 22 ++++++++++++++ 7 files changed, 176 insertions(+) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 8bb057288a653..7f1d5ac6225f6 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -117,6 +117,60 @@ def button(self, text): """ return self.gui.button(text) + def input_int(self, label, old_value): + """Integer input field with +/- buttons. + + Args: + label (str): Label for the input field. + old_value (int): Current value. + + Returns: + int: The updated value. + """ + return self.gui.input_int(label, old_value) + + def input_float(self, label, old_value): + """Float input field with +/- buttons. + + Args: + label (str): Label for the input field. + old_value (float): Current value. + + Returns: + float: The updated value. + """ + return self.gui.input_float(label, old_value) + + def drag_int(self, label, old_value, speed=1.0, minimum=0, maximum=0): + """Draggable integer input. + + Args: + label (str): Label for the input. + old_value (int): Current value. + speed (float): Drag speed multiplier. + minimum (int): Minimum value (0 for no limit). + maximum (int): Maximum value (0 for no limit). + + Returns: + int: The updated value. + """ + return self.gui.drag_int(label, old_value, speed, minimum, maximum) + + def drag_float(self, label, old_value, speed=1.0, minimum=0.0, maximum=0.0): + """Draggable float input. + + Args: + label (str): Label for the input. + old_value (float): Current value. + speed (float): Drag speed multiplier. + minimum (float): Minimum value (0.0 for no limit). + maximum (float): Maximum value (0.0 for no limit). + + Returns: + float: The updated value. + """ + return self.gui.drag_float(label, old_value, speed, minimum, maximum) + def combo(self, label, current_index, items): """Combo box (dropdown) for selecting from a tuple of items. diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 69a9b942c7424..510574d10398f 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -111,6 +111,26 @@ struct PyGui { bool button(std::string name) { return gui->button(name); } + int input_int(std::string label, int old_value) { + return gui->input_int(label, old_value); + } + float input_float(std::string label, float old_value) { + return gui->input_float(label, old_value); + } + int drag_int(std::string label, + int old_value, + float speed, + int minimum, + int maximum) { + return gui->drag_int(label, old_value, speed, minimum, maximum); + } + float drag_float(std::string label, + float old_value, + float speed, + float minimum, + float maximum) { + return gui->drag_float(label, old_value, speed, minimum, maximum); + } int combo(std::string label, int current_item, py::tuple items_py) { auto it = combo_cache_.find(label); @@ -758,6 +778,10 @@ void export_ggui(py::module &m) { .def("slider_float", &PyGui::slider_float) .def("color_edit_3", &PyGui::color_edit_3) .def("button", &PyGui::button) + .def("input_int", &PyGui::input_int) + .def("input_float", &PyGui::input_float) + .def("drag_int", &PyGui::drag_int) + .def("drag_float", &PyGui::drag_float) .def("combo", &PyGui::combo); py::class_(m, "PyScene") diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index a6ff5ab0eae14..83e9c19c3971b 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -30,6 +30,18 @@ class GuiBase { virtual int combo(const std::string &label, int current_item, const std::vector &items) = 0; + virtual int input_int(const std::string &label, int old_value) = 0; + virtual float input_float(const std::string &label, float old_value) = 0; + virtual int drag_int(const std::string &label, + int old_value, + float speed, + int minimum, + int maximum) = 0; + virtual float drag_float(const std::string &label, + float old_value, + float speed, + float minimum, + float maximum) = 0; virtual void prepare_for_next_frame() = 0; virtual ~GuiBase() = default; }; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index 0e94d10664d83..9c3f6262e9ae7 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -225,6 +225,46 @@ int Gui::combo(const std::string &label, return current_item; } +int Gui::input_int(const std::string &label, int old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputInt(label.c_str(), &old_value); + return old_value; +} + +float Gui::input_float(const std::string &label, float old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputFloat(label.c_str(), &old_value); + return old_value; +} + +int Gui::drag_int(const std::string &label, + int old_value, + float speed, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragInt(label.c_str(), &old_value, speed, minimum, maximum); + return old_value; +} + +float Gui::drag_float(const std::string &label, + float old_value, + float speed, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragFloat(label.c_str(), &old_value, speed, minimum, maximum); + return old_value; +} + void Gui::draw(taichi::lang::CommandList *cmd_list) { // Rendering ImGui::Render(); diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index 85e6b5916039b..f7ec6ce8d1f96 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -52,6 +52,18 @@ class TI_DLL_EXPORT Gui final : public GuiBase { int combo(const std::string &label, int current_item, const std::vector &items) override; + int input_int(const std::string &label, int old_value) override; + float input_float(const std::string &label, float old_value) override; + int drag_int(const std::string &label, + int old_value, + float speed, + int minimum, + int maximum) override; + float drag_float(const std::string &label, + float old_value, + float speed, + float minimum, + float maximum) override; void draw(taichi::lang::CommandList *cmd_list); diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index 23844342fba05..d77f21c790cf9 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -46,6 +46,18 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { int combo(const std::string &label, int current_item, const std::vector &items) override; + int input_int(const std::string &label, int old_value) override; + float input_float(const std::string &label, float old_value) override; + int drag_int(const std::string &label, + int old_value, + float speed, + int minimum, + int maximum) override; + float drag_float(const std::string &label, + float old_value, + float speed, + float minimum, + float maximum) override; void prepare_for_next_frame() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index ea89700416e12..37949d4cf734a 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -98,6 +98,28 @@ return current_item; } +int GuiMetal::input_int(const std::string &label, int old_value) { + ImGui::InputInt(label.c_str(), &old_value); + return old_value; +} + +float GuiMetal::input_float(const std::string &label, float old_value) { + ImGui::InputFloat(label.c_str(), &old_value); + return old_value; +} + +int GuiMetal::drag_int(const std::string &label, int old_value, float speed, + int minimum, int maximum) { + ImGui::DragInt(label.c_str(), &old_value, speed, minimum, maximum); + return old_value; +} + +float GuiMetal::drag_float(const std::string &label, float old_value, + float speed, float minimum, float maximum) { + ImGui::DragFloat(label.c_str(), &old_value, speed, minimum, maximum); + return old_value; +} + void GuiMetal::draw(taichi::lang::CommandList *cmd_list) { ImGui_ImplMetal_NewFrame(current_rpd_); From 4d10ac22cedec1fc3d85e07bf915d84d74a1590b Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Sun, 1 Feb 2026 23:42:25 -0700 Subject: [PATCH 3/9] tree node imgui binding --- python/taichi/ui/imgui.py | 47 +++++++++++++++++++++++++++++++++++ taichi/python/export_ggui.cpp | 8 ++++++ taichi/ui/common/gui_base.h | 2 ++ taichi/ui/ggui/gui.cpp | 14 +++++++++++ taichi/ui/ggui/gui.h | 2 ++ taichi/ui/ggui/gui_metal.h | 2 ++ taichi/ui/ggui/gui_metal.mm | 6 +++++ 7 files changed, 81 insertions(+) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 7f1d5ac6225f6..23d9c8a8b95e0 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -196,3 +196,50 @@ def combo(self, label, current_index, items): window.show() """ return self.gui.combo(label, current_index, items) + + def tree_node_push(self, label): + """Begin a collapsible tree node (low-level). + + Args: + label (str): Label for the tree node. + + Returns: + bool: True if the node is expanded, False if collapsed. + + Note: + You must call tree_node_pop() if this returns True. + Consider using tree_node() generator instead for automatic cleanup. + """ + return self.gui.tree_node_push(label) + + def tree_node_pop(self): + """End a tree node (low-level). + + Only call this if tree_node_push() returned True. + """ + self.gui.tree_node_pop() + + def tree_node(self, label): + """Collapsible tree node (generator). + + Use with a for loop - the body runs only if the node is expanded, + and tree_node_pop is called automatically. + + Args: + label (str): Label for the tree node. + + Yields: + Nothing, but loop body executes only if node is expanded. + + Example:: + + for _ in gui.tree_node("Settings"): + gui.slider_float("Volume", volume, 0, 1) + for _ in gui.tree_node("Advanced"): + gui.checkbox("Debug", debug) + """ + if self.gui.tree_node_push(label): + try: + yield + finally: + self.gui.tree_node_pop() diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 510574d10398f..0c184f2676ca3 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -131,6 +131,12 @@ struct PyGui { float maximum) { return gui->drag_float(label, old_value, speed, minimum, maximum); } + bool tree_node_push(std::string label) { + return gui->tree_node_push(label); + } + void tree_node_pop() { + gui->tree_node_pop(); + } int combo(std::string label, int current_item, py::tuple items_py) { auto it = combo_cache_.find(label); @@ -782,6 +788,8 @@ void export_ggui(py::module &m) { .def("input_float", &PyGui::input_float) .def("drag_int", &PyGui::drag_int) .def("drag_float", &PyGui::drag_float) + .def("tree_node_push", &PyGui::tree_node_push) + .def("tree_node_pop", &PyGui::tree_node_pop) .def("combo", &PyGui::combo); py::class_(m, "PyScene") diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index 83e9c19c3971b..9ec99724bbf91 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -42,6 +42,8 @@ class GuiBase { float speed, float minimum, float maximum) = 0; + virtual bool tree_node_push(const std::string &label) = 0; + virtual void tree_node_pop() = 0; virtual void prepare_for_next_frame() = 0; virtual ~GuiBase() = default; }; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index 9c3f6262e9ae7..88529d8be52ed 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -265,6 +265,20 @@ float Gui::drag_float(const std::string &label, return old_value; } +bool Gui::tree_node_push(const std::string &label) { + if (!initialized()) { + return false; + } + return ImGui::TreeNode(label.c_str()); +} + +void Gui::tree_node_pop() { + if (!initialized()) { + return; + } + ImGui::TreePop(); +} + void Gui::draw(taichi::lang::CommandList *cmd_list) { // Rendering ImGui::Render(); diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index f7ec6ce8d1f96..02c55ec49e6a0 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -64,6 +64,8 @@ class TI_DLL_EXPORT Gui final : public GuiBase { float speed, float minimum, float maximum) override; + bool tree_node_push(const std::string &label) override; + void tree_node_pop() override; void draw(taichi::lang::CommandList *cmd_list); diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index d77f21c790cf9..77d43653fa86a 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -58,6 +58,8 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { float speed, float minimum, float maximum) override; + bool tree_node_push(const std::string &label) override; + void tree_node_pop() override; void prepare_for_next_frame() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index 37949d4cf734a..225ed844931ba 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -120,6 +120,12 @@ return old_value; } +bool GuiMetal::tree_node_push(const std::string &label) { + return ImGui::TreeNode(label.c_str()); +} + +void GuiMetal::tree_node_pop() { ImGui::TreePop(); } + void GuiMetal::draw(taichi::lang::CommandList *cmd_list) { ImGui_ImplMetal_NewFrame(current_rpd_); From e4cc1f6b5356079365ff517daaf4644b73b4e7ee Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Sun, 1 Feb 2026 23:48:25 -0700 Subject: [PATCH 4/9] simple imgui bindings --- python/taichi/ui/imgui.py | 34 ++++++++++++++++++++++++++++++++++ taichi/python/export_ggui.cpp | 20 ++++++++++++++++++++ taichi/ui/common/gui_base.h | 5 +++++ taichi/ui/ggui/gui.cpp | 35 +++++++++++++++++++++++++++++++++++ taichi/ui/ggui/gui.h | 5 +++++ taichi/ui/ggui/gui_metal.h | 5 +++++ taichi/ui/ggui/gui_metal.mm | 10 ++++++++++ 7 files changed, 114 insertions(+) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 23d9c8a8b95e0..9e090b7db3cbd 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -243,3 +243,37 @@ def tree_node(self, label): yield finally: self.gui.tree_node_pop() + + def separator(self): + """Draw a horizontal separator line.""" + self.gui.separator() + + def same_line(self): + """Place the next widget on the same line as the previous one.""" + self.gui.same_line() + + @contextmanager + def indent(self): + """Indent subsequent widgets (context manager). + + Example:: + + gui.text("Parent") + with gui.indent(): + gui.text("Child 1") + gui.text("Child 2") + gui.text("Back to normal") + """ + self.gui.indent() + try: + yield + finally: + self.gui.unindent() + + def progress_bar(self, fraction): + """Display a progress bar. + + Args: + fraction (float): Progress value between 0.0 and 1.0. + """ + self.gui.progress_bar(fraction) diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 0c184f2676ca3..3bd8df81c3b1a 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -137,6 +137,21 @@ struct PyGui { void tree_node_pop() { gui->tree_node_pop(); } + void separator() { + gui->separator(); + } + void same_line() { + gui->same_line(); + } + void indent() { + gui->indent(); + } + void unindent() { + gui->unindent(); + } + void progress_bar(float fraction) { + gui->progress_bar(fraction); + } int combo(std::string label, int current_item, py::tuple items_py) { auto it = combo_cache_.find(label); @@ -790,6 +805,11 @@ void export_ggui(py::module &m) { .def("drag_float", &PyGui::drag_float) .def("tree_node_push", &PyGui::tree_node_push) .def("tree_node_pop", &PyGui::tree_node_pop) + .def("separator", &PyGui::separator) + .def("same_line", &PyGui::same_line) + .def("indent", &PyGui::indent) + .def("unindent", &PyGui::unindent) + .def("progress_bar", &PyGui::progress_bar) .def("combo", &PyGui::combo); py::class_(m, "PyScene") diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index 9ec99724bbf91..e36fc94cb5e06 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -44,6 +44,11 @@ class GuiBase { float maximum) = 0; virtual bool tree_node_push(const std::string &label) = 0; virtual void tree_node_pop() = 0; + virtual void separator() = 0; + virtual void same_line() = 0; + virtual void indent() = 0; + virtual void unindent() = 0; + virtual void progress_bar(float fraction) = 0; virtual void prepare_for_next_frame() = 0; virtual ~GuiBase() = default; }; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index 88529d8be52ed..ec6c3f62f46b2 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -279,6 +279,41 @@ void Gui::tree_node_pop() { ImGui::TreePop(); } +void Gui::separator() { + if (!initialized()) { + return; + } + ImGui::Separator(); +} + +void Gui::same_line() { + if (!initialized()) { + return; + } + ImGui::SameLine(); +} + +void Gui::indent() { + if (!initialized()) { + return; + } + ImGui::Indent(); +} + +void Gui::unindent() { + if (!initialized()) { + return; + } + ImGui::Unindent(); +} + +void Gui::progress_bar(float fraction) { + if (!initialized()) { + return; + } + ImGui::ProgressBar(fraction); +} + void Gui::draw(taichi::lang::CommandList *cmd_list) { // Rendering ImGui::Render(); diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index 02c55ec49e6a0..24a92d227cec7 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -66,6 +66,11 @@ class TI_DLL_EXPORT Gui final : public GuiBase { float maximum) override; bool tree_node_push(const std::string &label) override; void tree_node_pop() override; + void separator() override; + void same_line() override; + void indent() override; + void unindent() override; + void progress_bar(float fraction) override; void draw(taichi::lang::CommandList *cmd_list); diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index 77d43653fa86a..c6a66682ea947 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -60,6 +60,11 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { float maximum) override; bool tree_node_push(const std::string &label) override; void tree_node_pop() override; + void separator() override; + void same_line() override; + void indent() override; + void unindent() override; + void progress_bar(float fraction) override; void prepare_for_next_frame() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index 225ed844931ba..4f1f6e3f54998 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -126,6 +126,16 @@ void GuiMetal::tree_node_pop() { ImGui::TreePop(); } +void GuiMetal::separator() { ImGui::Separator(); } + +void GuiMetal::same_line() { ImGui::SameLine(); } + +void GuiMetal::indent() { ImGui::Indent(); } + +void GuiMetal::unindent() { ImGui::Unindent(); } + +void GuiMetal::progress_bar(float fraction) { ImGui::ProgressBar(fraction); } + void GuiMetal::draw(taichi::lang::CommandList *cmd_list) { ImGui_ImplMetal_NewFrame(current_rpd_); From 37c4a99ffbac2494adba796ff48deba19da3e1bd Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Sun, 1 Feb 2026 23:59:48 -0700 Subject: [PATCH 5/9] radio, header, selectable, listbox, tab imgui bindings --- python/taichi/ui/imgui.py | 161 ++++++++++++++++++++++++++++++++++ taichi/python/export_ggui.cpp | 55 +++++++++++- taichi/ui/common/gui_base.h | 11 +++ taichi/ui/ggui/gui.cpp | 62 +++++++++++++ taichi/ui/ggui/gui.h | 11 +++ taichi/ui/ggui/gui_metal.h | 11 +++ taichi/ui/ggui/gui_metal.mm | 33 +++++++ 7 files changed, 343 insertions(+), 1 deletion(-) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 9e090b7db3cbd..2f1c1099ad4c5 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -277,3 +277,164 @@ def progress_bar(self, fraction): fraction (float): Progress value between 0.0 and 1.0. """ self.gui.progress_bar(fraction) + + def collapsing_header(self, label): + """A collapsible header that reveals content when expanded. + + Args: + label (str): Label for the header. + + Returns: + bool: True if the header is expanded, False if collapsed. + + Example:: + + if gui.collapsing_header("Advanced Settings"): + gui.slider_float("Detail", detail, 0, 1) + """ + return self.gui.collapsing_header(label) + + def selectable(self, label, selected=False): + """A selectable item (like a list entry). + + Args: + label (str): Label for the item. + selected (bool): Whether the item is currently selected. + + Returns: + bool: The new selected state (toggled if clicked). + """ + return self.gui.selectable(label, selected) + + def radio_button(self, label, active): + """A radio button. + + Args: + label (str): Label for the button. + active (bool): Whether this option is currently selected. + + Returns: + bool: True if clicked (use to update selection state). + + Example:: + + if gui.radio_button("Option A", choice == 0): + choice = 0 + if gui.radio_button("Option B", choice == 1): + choice = 1 + """ + return self.gui.radio_button(label, active) + + def listbox(self, label, current_index, items, height_in_items=-1): + """A listbox for selecting from a tuple of items. + + Args: + label (str): Label for the listbox. + current_index (int): Currently selected index (0-based). + items (tuple[str, ...]): Tuple of string options. + height_in_items (int): Number of items to show (-1 for default). + + Returns: + int: The newly selected index. + + Note: + Like combo(), uses caching for tuple→string conversion. + Define items as module-level constants for best performance. + """ + return self.gui.listbox(label, current_index, items, height_in_items) + + def begin_tab_bar(self, bar_id): + """Begin a tab bar (low-level). + + Args: + bar_id (str): Unique identifier for the tab bar. + + Returns: + bool: True if the tab bar is visible. + + Note: + Must call end_tab_bar() if this returns True. + Consider using tab_bar() generator instead. + """ + return self.gui.begin_tab_bar(bar_id) + + def end_tab_bar(self): + """End a tab bar (low-level). + + Only call this if begin_tab_bar() returned True. + """ + self.gui.end_tab_bar() + + def begin_tab_item(self, label): + """Begin a tab item (low-level). + + Args: + label (str): Label for the tab. + + Returns: + bool: True if this tab is selected. + + Note: + Must call end_tab_item() if this returns True. + Consider using tab_item() generator instead. + """ + return self.gui.begin_tab_item(label) + + def end_tab_item(self): + """End a tab item (low-level). + + Only call this if begin_tab_item() returned True. + """ + self.gui.end_tab_item() + + def tab_bar(self, bar_id): + """Tab bar container (generator). + + Use with a for loop - the body runs only if the tab bar is visible, + and end_tab_bar is called automatically. + + Args: + bar_id (str): Unique identifier for the tab bar. + + Yields: + Nothing, but loop body executes only if visible. + + Example:: + + for _ in gui.tab_bar("my_tabs"): + for _ in gui.tab_item("Tab 1"): + gui.text("Content for tab 1") + for _ in gui.tab_item("Tab 2"): + gui.text("Content for tab 2") + """ + if self.gui.begin_tab_bar(bar_id): + try: + yield + finally: + self.gui.end_tab_bar() + + def tab_item(self, label): + """Tab item (generator). + + Use with a for loop inside tab_bar - the body runs only if this tab + is selected, and end_tab_item is called automatically. + + Args: + label (str): Label for the tab. + + Yields: + Nothing, but loop body executes only if tab is selected. + + Example:: + + for _ in gui.tab_bar("settings"): + for _ in gui.tab_item("General"): + gui.checkbox("Enable feature", enabled) + for _ in gui.tab_item("Advanced"): + gui.slider_int("Level", level, 1, 10) + """ + if self.gui.begin_tab_item(label): + try: + yield + finally: + self.gui.end_tab_item() diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 3bd8df81c3b1a..029a919c1cd26 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -152,6 +152,27 @@ struct PyGui { void progress_bar(float fraction) { gui->progress_bar(fraction); } + bool collapsing_header(std::string label) { + return gui->collapsing_header(label); + } + bool selectable(std::string label, bool selected) { + return gui->selectable(label, selected); + } + bool radio_button(std::string label, bool active) { + return gui->radio_button(label, active); + } + bool begin_tab_bar(std::string id) { + return gui->begin_tab_bar(id); + } + void end_tab_bar() { + gui->end_tab_bar(); + } + bool begin_tab_item(std::string label) { + return gui->begin_tab_item(label); + } + void end_tab_item() { + gui->end_tab_item(); + } int combo(std::string label, int current_item, py::tuple items_py) { auto it = combo_cache_.find(label); @@ -173,6 +194,30 @@ struct PyGui { return gui->combo(label, current_item, it->second.items_cstr); } + int listbox(std::string label, + int current_item, + py::tuple items_py, + int height_in_items) { + // Use same cache as combo (keyed by label) + auto it = combo_cache_.find(label); + + if (it == combo_cache_.end() || !it->second.items_tuple.is(items_py)) { + ComboCache cache; + cache.items_tuple = items_py; + for (auto item : items_py) { + cache.items_str.push_back(item.cast()); + } + for (const auto &s : cache.items_str) { + cache.items_cstr.push_back(s.c_str()); + } + combo_cache_[label] = std::move(cache); + it = combo_cache_.find(label); + } + it->second.touched = true; + return gui->listbox(label, current_item, it->second.items_cstr, + height_in_items); + } + // Called at frame end to clean up stale cache entries void frame_end() { for (auto it = combo_cache_.begin(); it != combo_cache_.end();) { @@ -810,7 +855,15 @@ void export_ggui(py::module &m) { .def("indent", &PyGui::indent) .def("unindent", &PyGui::unindent) .def("progress_bar", &PyGui::progress_bar) - .def("combo", &PyGui::combo); + .def("combo", &PyGui::combo) + .def("collapsing_header", &PyGui::collapsing_header) + .def("selectable", &PyGui::selectable) + .def("radio_button", &PyGui::radio_button) + .def("listbox", &PyGui::listbox) + .def("begin_tab_bar", &PyGui::begin_tab_bar) + .def("end_tab_bar", &PyGui::end_tab_bar) + .def("begin_tab_item", &PyGui::begin_tab_item) + .def("end_tab_item", &PyGui::end_tab_item); py::class_(m, "PyScene") .def(py::init<>()) diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index e36fc94cb5e06..f62104290b34a 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -49,6 +49,17 @@ class GuiBase { virtual void indent() = 0; virtual void unindent() = 0; virtual void progress_bar(float fraction) = 0; + virtual bool collapsing_header(const std::string &label) = 0; + virtual bool selectable(const std::string &label, bool selected) = 0; + virtual bool radio_button(const std::string &label, bool active) = 0; + virtual int listbox(const std::string &label, + int current_item, + const std::vector &items, + int height_in_items) = 0; + virtual bool begin_tab_bar(const std::string &id) = 0; + virtual void end_tab_bar() = 0; + virtual bool begin_tab_item(const std::string &label) = 0; + virtual void end_tab_item() = 0; virtual void prepare_for_next_frame() = 0; virtual ~GuiBase() = default; }; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index ec6c3f62f46b2..122f54bb42131 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -314,6 +314,68 @@ void Gui::progress_bar(float fraction) { ImGui::ProgressBar(fraction); } +bool Gui::collapsing_header(const std::string &label) { + if (!initialized()) { + return false; + } + return ImGui::CollapsingHeader(label.c_str()); +} + +bool Gui::selectable(const std::string &label, bool selected) { + if (!initialized()) { + return selected; + } + ImGui::Selectable(label.c_str(), &selected); + return selected; +} + +bool Gui::radio_button(const std::string &label, bool active) { + if (!initialized()) { + return false; + } + return ImGui::RadioButton(label.c_str(), active); +} + +int Gui::listbox(const std::string &label, + int current_item, + const std::vector &items, + int height_in_items) { + if (!initialized()) { + return current_item; + } + ImGui::ListBox(label.c_str(), ¤t_item, items.data(), + static_cast(items.size()), height_in_items); + return current_item; +} + +bool Gui::begin_tab_bar(const std::string &id) { + if (!initialized()) { + return false; + } + return ImGui::BeginTabBar(id.c_str()); +} + +void Gui::end_tab_bar() { + if (!initialized()) { + return; + } + ImGui::EndTabBar(); +} + +bool Gui::begin_tab_item(const std::string &label) { + if (!initialized()) { + return false; + } + return ImGui::BeginTabItem(label.c_str()); +} + +void Gui::end_tab_item() { + if (!initialized()) { + return; + } + ImGui::EndTabItem(); +} + void Gui::draw(taichi::lang::CommandList *cmd_list) { // Rendering ImGui::Render(); diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index 24a92d227cec7..c7dba85cd94b7 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -71,6 +71,17 @@ class TI_DLL_EXPORT Gui final : public GuiBase { void indent() override; void unindent() override; void progress_bar(float fraction) override; + bool collapsing_header(const std::string &label) override; + bool selectable(const std::string &label, bool selected) override; + bool radio_button(const std::string &label, bool active) override; + int listbox(const std::string &label, + int current_item, + const std::vector &items, + int height_in_items) override; + bool begin_tab_bar(const std::string &id) override; + void end_tab_bar() override; + bool begin_tab_item(const std::string &label) override; + void end_tab_item() override; void draw(taichi::lang::CommandList *cmd_list); diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index c6a66682ea947..657a5478bbc99 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -65,6 +65,17 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { void indent() override; void unindent() override; void progress_bar(float fraction) override; + bool collapsing_header(const std::string &label) override; + bool selectable(const std::string &label, bool selected) override; + bool radio_button(const std::string &label, bool active) override; + int listbox(const std::string &label, + int current_item, + const std::vector &items, + int height_in_items) override; + bool begin_tab_bar(const std::string &id) override; + void end_tab_bar() override; + bool begin_tab_item(const std::string &label) override; + void end_tab_item() override; void prepare_for_next_frame() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index 4f1f6e3f54998..b1c2187a2104b 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -136,6 +136,39 @@ void GuiMetal::progress_bar(float fraction) { ImGui::ProgressBar(fraction); } +bool GuiMetal::collapsing_header(const std::string &label) { + return ImGui::CollapsingHeader(label.c_str()); +} + +bool GuiMetal::selectable(const std::string &label, bool selected) { + ImGui::Selectable(label.c_str(), &selected); + return selected; +} + +bool GuiMetal::radio_button(const std::string &label, bool active) { + return ImGui::RadioButton(label.c_str(), active); +} + +int GuiMetal::listbox(const std::string &label, int current_item, + const std::vector &items, + int height_in_items) { + ImGui::ListBox(label.c_str(), ¤t_item, items.data(), + static_cast(items.size()), height_in_items); + return current_item; +} + +bool GuiMetal::begin_tab_bar(const std::string &id) { + return ImGui::BeginTabBar(id.c_str()); +} + +void GuiMetal::end_tab_bar() { ImGui::EndTabBar(); } + +bool GuiMetal::begin_tab_item(const std::string &label) { + return ImGui::BeginTabItem(label.c_str()); +} + +void GuiMetal::end_tab_item() { ImGui::EndTabItem(); } + void GuiMetal::draw(taichi::lang::CommandList *cmd_list) { ImGui_ImplMetal_NewFrame(current_rpd_); From 350861648b232cd3db8357dc932c0fd7f96edb4d Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Mon, 2 Feb 2026 00:13:05 -0700 Subject: [PATCH 6/9] vector input widgets imgui bindings --- python/taichi/ui/imgui.py | 125 ++++++++++++++++++---- taichi/python/export_ggui.cpp | 174 +++++++++++++++++++++++++++++++ taichi/ui/common/gui_base.h | 68 ++++++++++++ taichi/ui/ggui/gui.cpp | 190 ++++++++++++++++++++++++++++++++++ taichi/ui/ggui/gui.h | 68 +++++++++++- taichi/ui/ggui/gui_metal.h | 68 +++++++++++- taichi/ui/ggui/gui_metal.mm | 109 +++++++++++++++++++ 7 files changed, 782 insertions(+), 20 deletions(-) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 2f1c1099ad4c5..e0e1863ea3a1b 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -75,37 +75,86 @@ def checkbox(self, text, old_value): return self.gui.checkbox(text, old_value) def slider_int(self, text, old_value, minimum, maximum): - """Declares a slider, and returns its newest value. + """Declares an integer slider, and returns its newest value. Args: - text (str): a line of text to be shown next to the slider - old_value (int) : the current value of the slider. + text (str): a line of text to be shown next to the slider. + old_value (int or tuple): the current value. Can be a scalar int + or a tuple of 2-4 ints for multi-component sliders. minimum (int): the minimum value of the slider. maximum (int): the maximum value of the slider. Returns: - int: the updated value of the slider. + int or tuple: the updated value (same type as input). """ + if isinstance(old_value, (tuple, list)): + n = len(old_value) + if n == 2: + return self.gui.slider_int2(text, tuple(old_value), minimum, maximum) + if n == 3: + return self.gui.slider_int3(text, tuple(old_value), minimum, maximum) + if n == 4: + return self.gui.slider_int4(text, tuple(old_value), minimum, maximum) + raise ValueError(f"slider_int expects 1-4 components, got {n}") return self.gui.slider_int(text, old_value, minimum, maximum) def slider_float(self, text, old_value, minimum, maximum): - """Declares a slider, and returns its newest value. + """Declares a float slider, and returns its newest value. Args: - text (str): a line of text to be shown next to the slider - old_value (float): the current value of the slider. + text (str): a line of text to be shown next to the slider. + old_value (float or tuple): the current value. Can be a scalar float + or a tuple of 2-4 floats for multi-component sliders. minimum (float): the minimum value of the slider. maximum (float): the maximum value of the slider. + + Returns: + float or tuple: the updated value (same type as input). """ + if isinstance(old_value, (tuple, list)): + n = len(old_value) + if n == 2: + return self.gui.slider_float2(text, tuple(old_value), minimum, maximum) + if n == 3: + return self.gui.slider_float3(text, tuple(old_value), minimum, maximum) + if n == 4: + return self.gui.slider_float4(text, tuple(old_value), minimum, maximum) + raise ValueError(f"slider_float expects 1-4 components, got {n}") return self.gui.slider_float(text, old_value, minimum, maximum) + def color_edit(self, text, old_value): + """Declares a color edit palette. + + Auto-detects RGB vs RGBA based on tuple size. + + Args: + text (str): a line of text to be shown next to the palette. + old_value (tuple): the current color value. Can be a tuple of + 3 floats (RGB) or 4 floats (RGBA) in [0,1]. + + Returns: + tuple: the updated color (same size as input). + """ + n = len(old_value) + if n == 3: + return self.gui.color_edit_3(text, tuple(old_value)) + if n == 4: + return self.gui.color_edit_4(text, tuple(old_value)) + raise ValueError(f"color_edit expects 3 (RGB) or 4 (RGBA) components, got {n}") + def color_edit_3(self, text, old_value): - """Declares a color edit palate. + """Declares an RGB color edit palette. + + Note: + Prefer using :meth:`color_edit` which auto-detects RGB/RGBA. Args: - text (str): a line of text to be shown next to the palate. + text (str): a line of text to be shown next to the palette. old_value (Tuple[float]): the current value of the color, this \ - should be a tuple of floats in [0,1] that indicates RGB values. + should be a tuple of 3 floats in [0,1] that indicates RGB values. + + Returns: + tuple: the updated RGB color as (r, g, b). """ return self.gui.color_edit_3(text, old_value) @@ -122,11 +171,21 @@ def input_int(self, label, old_value): Args: label (str): Label for the input field. - old_value (int): Current value. + old_value (int or tuple): Current value. Can be a scalar int + or a tuple of 2-4 ints for multi-component inputs. Returns: - int: The updated value. + int or tuple: The updated value (same type as input). """ + if isinstance(old_value, (tuple, list)): + n = len(old_value) + if n == 2: + return self.gui.input_int2(label, tuple(old_value)) + if n == 3: + return self.gui.input_int3(label, tuple(old_value)) + if n == 4: + return self.gui.input_int4(label, tuple(old_value)) + raise ValueError(f"input_int expects 1-4 components, got {n}") return self.gui.input_int(label, old_value) def input_float(self, label, old_value): @@ -134,11 +193,21 @@ def input_float(self, label, old_value): Args: label (str): Label for the input field. - old_value (float): Current value. + old_value (float or tuple): Current value. Can be a scalar float + or a tuple of 2-4 floats for multi-component inputs. Returns: - float: The updated value. + float or tuple: The updated value (same type as input). """ + if isinstance(old_value, (tuple, list)): + n = len(old_value) + if n == 2: + return self.gui.input_float2(label, tuple(old_value)) + if n == 3: + return self.gui.input_float3(label, tuple(old_value)) + if n == 4: + return self.gui.input_float4(label, tuple(old_value)) + raise ValueError(f"input_float expects 1-4 components, got {n}") return self.gui.input_float(label, old_value) def drag_int(self, label, old_value, speed=1.0, minimum=0, maximum=0): @@ -146,14 +215,24 @@ def drag_int(self, label, old_value, speed=1.0, minimum=0, maximum=0): Args: label (str): Label for the input. - old_value (int): Current value. + old_value (int or tuple): Current value. Can be a scalar int + or a tuple of 2-4 ints for multi-component inputs. speed (float): Drag speed multiplier. minimum (int): Minimum value (0 for no limit). maximum (int): Maximum value (0 for no limit). Returns: - int: The updated value. + int or tuple: The updated value (same type as input). """ + if isinstance(old_value, (tuple, list)): + n = len(old_value) + if n == 2: + return self.gui.drag_int2(label, tuple(old_value), speed, minimum, maximum) + if n == 3: + return self.gui.drag_int3(label, tuple(old_value), speed, minimum, maximum) + if n == 4: + return self.gui.drag_int4(label, tuple(old_value), speed, minimum, maximum) + raise ValueError(f"drag_int expects 1-4 components, got {n}") return self.gui.drag_int(label, old_value, speed, minimum, maximum) def drag_float(self, label, old_value, speed=1.0, minimum=0.0, maximum=0.0): @@ -161,14 +240,24 @@ def drag_float(self, label, old_value, speed=1.0, minimum=0.0, maximum=0.0): Args: label (str): Label for the input. - old_value (float): Current value. + old_value (float or tuple): Current value. Can be a scalar float + or a tuple of 2-4 floats for multi-component inputs. speed (float): Drag speed multiplier. minimum (float): Minimum value (0.0 for no limit). maximum (float): Maximum value (0.0 for no limit). Returns: - float: The updated value. + float or tuple: The updated value (same type as input). """ + if isinstance(old_value, (tuple, list)): + n = len(old_value) + if n == 2: + return self.gui.drag_float2(label, tuple(old_value), speed, minimum, maximum) + if n == 3: + return self.gui.drag_float3(label, tuple(old_value), speed, minimum, maximum) + if n == 4: + return self.gui.drag_float4(label, tuple(old_value), speed, minimum, maximum) + raise ValueError(f"drag_float expects 1-4 components, got {n}") return self.gui.drag_float(label, old_value, speed, minimum, maximum) def combo(self, label, current_index, items): diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 029a919c1cd26..0b60446ae4f85 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -36,14 +36,56 @@ namespace taichi::ui { using namespace taichi::lang; +glm::vec2 tuple_to_vec2(pybind11::tuple t) { + return glm::vec2(t[0].cast(), t[1].cast()); +} + glm::vec3 tuple_to_vec3(pybind11::tuple t) { return glm::vec3(t[0].cast(), t[1].cast(), t[2].cast()); } +glm::vec4 tuple_to_vec4(pybind11::tuple t) { + return glm::vec4(t[0].cast(), t[1].cast(), t[2].cast(), + t[3].cast()); +} + +glm::ivec2 tuple_to_ivec2(pybind11::tuple t) { + return glm::ivec2(t[0].cast(), t[1].cast()); +} + +glm::ivec3 tuple_to_ivec3(pybind11::tuple t) { + return glm::ivec3(t[0].cast(), t[1].cast(), t[2].cast()); +} + +glm::ivec4 tuple_to_ivec4(pybind11::tuple t) { + return glm::ivec4(t[0].cast(), t[1].cast(), t[2].cast(), + t[3].cast()); +} + +pybind11::tuple vec2_to_tuple(glm::vec2 v) { + return pybind11::make_tuple(v.x, v.y); +} + pybind11::tuple vec3_to_tuple(glm::vec3 v) { return pybind11::make_tuple(v.x, v.y, v.z); } +pybind11::tuple vec4_to_tuple(glm::vec4 v) { + return pybind11::make_tuple(v.x, v.y, v.z, v.w); +} + +pybind11::tuple ivec2_to_tuple(glm::ivec2 v) { + return pybind11::make_tuple(v.x, v.y); +} + +pybind11::tuple ivec3_to_tuple(glm::ivec3 v) { + return pybind11::make_tuple(v.x, v.y, v.z); +} + +pybind11::tuple ivec4_to_tuple(glm::ivec4 v) { + return pybind11::make_tuple(v.x, v.y, v.z, v.w); +} + // Here we convert the 2d-array to numpy array using pybind. Refs: // https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html?highlight=array_t#vectorizing-functions // https://stackoverflow.com/questions/44659924/returning-numpy-arrays-via-pybind11 @@ -97,26 +139,91 @@ struct PyGui { int slider_int(std::string name, int old_value, int minimum, int maximum) { return gui->slider_int(name, old_value, minimum, maximum); } + py::tuple slider_int2(std::string name, + py::tuple old_value, + int minimum, + int maximum) { + return ivec2_to_tuple( + gui->slider_int2(name, tuple_to_ivec2(old_value), minimum, maximum)); + } + py::tuple slider_int3(std::string name, + py::tuple old_value, + int minimum, + int maximum) { + return ivec3_to_tuple( + gui->slider_int3(name, tuple_to_ivec3(old_value), minimum, maximum)); + } + py::tuple slider_int4(std::string name, + py::tuple old_value, + int minimum, + int maximum) { + return ivec4_to_tuple( + gui->slider_int4(name, tuple_to_ivec4(old_value), minimum, maximum)); + } float slider_float(std::string name, float old_value, float minimum, float maximum) { return gui->slider_float(name, old_value, minimum, maximum); } + py::tuple slider_float2(std::string name, + py::tuple old_value, + float minimum, + float maximum) { + return vec2_to_tuple( + gui->slider_float2(name, tuple_to_vec2(old_value), minimum, maximum)); + } + py::tuple slider_float3(std::string name, + py::tuple old_value, + float minimum, + float maximum) { + return vec3_to_tuple( + gui->slider_float3(name, tuple_to_vec3(old_value), minimum, maximum)); + } + py::tuple slider_float4(std::string name, + py::tuple old_value, + float minimum, + float maximum) { + return vec4_to_tuple( + gui->slider_float4(name, tuple_to_vec4(old_value), minimum, maximum)); + } py::tuple color_edit_3(std::string name, py::tuple old_value) { glm::vec3 old_color = tuple_to_vec3(old_value); glm::vec3 new_color = gui->color_edit_3(name, old_color); return vec3_to_tuple(new_color); } + py::tuple color_edit_4(std::string name, py::tuple old_value) { + glm::vec4 old_color = tuple_to_vec4(old_value); + glm::vec4 new_color = gui->color_edit_4(name, old_color); + return vec4_to_tuple(new_color); + } bool button(std::string name) { return gui->button(name); } int input_int(std::string label, int old_value) { return gui->input_int(label, old_value); } + py::tuple input_int2(std::string label, py::tuple old_value) { + return ivec2_to_tuple(gui->input_int2(label, tuple_to_ivec2(old_value))); + } + py::tuple input_int3(std::string label, py::tuple old_value) { + return ivec3_to_tuple(gui->input_int3(label, tuple_to_ivec3(old_value))); + } + py::tuple input_int4(std::string label, py::tuple old_value) { + return ivec4_to_tuple(gui->input_int4(label, tuple_to_ivec4(old_value))); + } float input_float(std::string label, float old_value) { return gui->input_float(label, old_value); } + py::tuple input_float2(std::string label, py::tuple old_value) { + return vec2_to_tuple(gui->input_float2(label, tuple_to_vec2(old_value))); + } + py::tuple input_float3(std::string label, py::tuple old_value) { + return vec3_to_tuple(gui->input_float3(label, tuple_to_vec3(old_value))); + } + py::tuple input_float4(std::string label, py::tuple old_value) { + return vec4_to_tuple(gui->input_float4(label, tuple_to_vec4(old_value))); + } int drag_int(std::string label, int old_value, float speed, @@ -124,6 +231,30 @@ struct PyGui { int maximum) { return gui->drag_int(label, old_value, speed, minimum, maximum); } + py::tuple drag_int2(std::string label, + py::tuple old_value, + float speed, + int minimum, + int maximum) { + return ivec2_to_tuple(gui->drag_int2(label, tuple_to_ivec2(old_value), + speed, minimum, maximum)); + } + py::tuple drag_int3(std::string label, + py::tuple old_value, + float speed, + int minimum, + int maximum) { + return ivec3_to_tuple(gui->drag_int3(label, tuple_to_ivec3(old_value), + speed, minimum, maximum)); + } + py::tuple drag_int4(std::string label, + py::tuple old_value, + float speed, + int minimum, + int maximum) { + return ivec4_to_tuple(gui->drag_int4(label, tuple_to_ivec4(old_value), + speed, minimum, maximum)); + } float drag_float(std::string label, float old_value, float speed, @@ -131,6 +262,30 @@ struct PyGui { float maximum) { return gui->drag_float(label, old_value, speed, minimum, maximum); } + py::tuple drag_float2(std::string label, + py::tuple old_value, + float speed, + float minimum, + float maximum) { + return vec2_to_tuple(gui->drag_float2(label, tuple_to_vec2(old_value), + speed, minimum, maximum)); + } + py::tuple drag_float3(std::string label, + py::tuple old_value, + float speed, + float minimum, + float maximum) { + return vec3_to_tuple(gui->drag_float3(label, tuple_to_vec3(old_value), + speed, minimum, maximum)); + } + py::tuple drag_float4(std::string label, + py::tuple old_value, + float speed, + float minimum, + float maximum) { + return vec4_to_tuple(gui->drag_float4(label, tuple_to_vec4(old_value), + speed, minimum, maximum)); + } bool tree_node_push(std::string label) { return gui->tree_node_push(label); } @@ -841,13 +996,32 @@ void export_ggui(py::module &m) { .def("text_colored", &PyGui::text_colored) .def("checkbox", &PyGui::checkbox) .def("slider_int", &PyGui::slider_int) + .def("slider_int2", &PyGui::slider_int2) + .def("slider_int3", &PyGui::slider_int3) + .def("slider_int4", &PyGui::slider_int4) .def("slider_float", &PyGui::slider_float) + .def("slider_float2", &PyGui::slider_float2) + .def("slider_float3", &PyGui::slider_float3) + .def("slider_float4", &PyGui::slider_float4) .def("color_edit_3", &PyGui::color_edit_3) + .def("color_edit_4", &PyGui::color_edit_4) .def("button", &PyGui::button) .def("input_int", &PyGui::input_int) + .def("input_int2", &PyGui::input_int2) + .def("input_int3", &PyGui::input_int3) + .def("input_int4", &PyGui::input_int4) .def("input_float", &PyGui::input_float) + .def("input_float2", &PyGui::input_float2) + .def("input_float3", &PyGui::input_float3) + .def("input_float4", &PyGui::input_float4) .def("drag_int", &PyGui::drag_int) + .def("drag_int2", &PyGui::drag_int2) + .def("drag_int3", &PyGui::drag_int3) + .def("drag_int4", &PyGui::drag_int4) .def("drag_float", &PyGui::drag_float) + .def("drag_float2", &PyGui::drag_float2) + .def("drag_float3", &PyGui::drag_float3) + .def("drag_float4", &PyGui::drag_float4) .def("tree_node_push", &PyGui::tree_node_push) .def("tree_node_pop", &PyGui::tree_node_pop) .def("separator", &PyGui::separator) diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index f62104290b34a..540884373b4e7 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -20,28 +20,96 @@ class GuiBase { int old_value, int minimum, int maximum) = 0; + virtual glm::ivec2 slider_int2(const std::string &name, + glm::ivec2 old_value, + int minimum, + int maximum) = 0; + virtual glm::ivec3 slider_int3(const std::string &name, + glm::ivec3 old_value, + int minimum, + int maximum) = 0; + virtual glm::ivec4 slider_int4(const std::string &name, + glm::ivec4 old_value, + int minimum, + int maximum) = 0; virtual float slider_float(const std::string &name, float old_value, float minimum, float maximum) = 0; + virtual glm::vec2 slider_float2(const std::string &name, + glm::vec2 old_value, + float minimum, + float maximum) = 0; + virtual glm::vec3 slider_float3(const std::string &name, + glm::vec3 old_value, + float minimum, + float maximum) = 0; + virtual glm::vec4 slider_float4(const std::string &name, + glm::vec4 old_value, + float minimum, + float maximum) = 0; virtual glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) = 0; + virtual glm::vec4 color_edit_4(const std::string &name, + glm::vec4 old_value) = 0; virtual bool button(const std::string &text) = 0; virtual int combo(const std::string &label, int current_item, const std::vector &items) = 0; virtual int input_int(const std::string &label, int old_value) = 0; + virtual glm::ivec2 input_int2(const std::string &label, + glm::ivec2 old_value) = 0; + virtual glm::ivec3 input_int3(const std::string &label, + glm::ivec3 old_value) = 0; + virtual glm::ivec4 input_int4(const std::string &label, + glm::ivec4 old_value) = 0; virtual float input_float(const std::string &label, float old_value) = 0; + virtual glm::vec2 input_float2(const std::string &label, + glm::vec2 old_value) = 0; + virtual glm::vec3 input_float3(const std::string &label, + glm::vec3 old_value) = 0; + virtual glm::vec4 input_float4(const std::string &label, + glm::vec4 old_value) = 0; virtual int drag_int(const std::string &label, int old_value, float speed, int minimum, int maximum) = 0; + virtual glm::ivec2 drag_int2(const std::string &label, + glm::ivec2 old_value, + float speed, + int minimum, + int maximum) = 0; + virtual glm::ivec3 drag_int3(const std::string &label, + glm::ivec3 old_value, + float speed, + int minimum, + int maximum) = 0; + virtual glm::ivec4 drag_int4(const std::string &label, + glm::ivec4 old_value, + float speed, + int minimum, + int maximum) = 0; virtual float drag_float(const std::string &label, float old_value, float speed, float minimum, float maximum) = 0; + virtual glm::vec2 drag_float2(const std::string &label, + glm::vec2 old_value, + float speed, + float minimum, + float maximum) = 0; + virtual glm::vec3 drag_float3(const std::string &label, + glm::vec3 old_value, + float speed, + float minimum, + float maximum) = 0; + virtual glm::vec4 drag_float4(const std::string &label, + glm::vec4 old_value, + float speed, + float minimum, + float maximum) = 0; virtual bool tree_node_push(const std::string &label) = 0; virtual void tree_node_pop() = 0; virtual void separator() = 0; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index 122f54bb42131..add771ba7da7d 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -190,6 +190,36 @@ int Gui::slider_int(const std::string &name, ImGui::SliderInt(name.c_str(), &old_value, minimum, maximum); return old_value; } +glm::ivec2 Gui::slider_int2(const std::string &name, + glm::ivec2 old_value, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::SliderInt2(name.c_str(), (int *)&old_value, minimum, maximum); + return old_value; +} +glm::ivec3 Gui::slider_int3(const std::string &name, + glm::ivec3 old_value, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::SliderInt3(name.c_str(), (int *)&old_value, minimum, maximum); + return old_value; +} +glm::ivec4 Gui::slider_int4(const std::string &name, + glm::ivec4 old_value, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::SliderInt4(name.c_str(), (int *)&old_value, minimum, maximum); + return old_value; +} float Gui::slider_float(const std::string &name, float old_value, float minimum, @@ -200,6 +230,36 @@ float Gui::slider_float(const std::string &name, ImGui::SliderFloat(name.c_str(), &old_value, minimum, maximum); return old_value; } +glm::vec2 Gui::slider_float2(const std::string &name, + glm::vec2 old_value, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::SliderFloat2(name.c_str(), (float *)&old_value, minimum, maximum); + return old_value; +} +glm::vec3 Gui::slider_float3(const std::string &name, + glm::vec3 old_value, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::SliderFloat3(name.c_str(), (float *)&old_value, minimum, maximum); + return old_value; +} +glm::vec4 Gui::slider_float4(const std::string &name, + glm::vec4 old_value, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::SliderFloat4(name.c_str(), (float *)&old_value, minimum, maximum); + return old_value; +} glm::vec3 Gui::color_edit_3(const std::string &name, glm::vec3 old_value) { if (!initialized()) { return old_value; @@ -207,6 +267,13 @@ glm::vec3 Gui::color_edit_3(const std::string &name, glm::vec3 old_value) { ImGui::ColorEdit3(name.c_str(), (float *)&old_value); return old_value; } +glm::vec4 Gui::color_edit_4(const std::string &name, glm::vec4 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::ColorEdit4(name.c_str(), (float *)&old_value); + return old_value; +} bool Gui::button(const std::string &text) { if (!initialized()) { return false; @@ -233,6 +300,30 @@ int Gui::input_int(const std::string &label, int old_value) { return old_value; } +glm::ivec2 Gui::input_int2(const std::string &label, glm::ivec2 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputInt2(label.c_str(), (int *)&old_value); + return old_value; +} + +glm::ivec3 Gui::input_int3(const std::string &label, glm::ivec3 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputInt3(label.c_str(), (int *)&old_value); + return old_value; +} + +glm::ivec4 Gui::input_int4(const std::string &label, glm::ivec4 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputInt4(label.c_str(), (int *)&old_value); + return old_value; +} + float Gui::input_float(const std::string &label, float old_value) { if (!initialized()) { return old_value; @@ -241,6 +332,30 @@ float Gui::input_float(const std::string &label, float old_value) { return old_value; } +glm::vec2 Gui::input_float2(const std::string &label, glm::vec2 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputFloat2(label.c_str(), (float *)&old_value); + return old_value; +} + +glm::vec3 Gui::input_float3(const std::string &label, glm::vec3 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputFloat3(label.c_str(), (float *)&old_value); + return old_value; +} + +glm::vec4 Gui::input_float4(const std::string &label, glm::vec4 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::InputFloat4(label.c_str(), (float *)&old_value); + return old_value; +} + int Gui::drag_int(const std::string &label, int old_value, float speed, @@ -253,6 +368,42 @@ int Gui::drag_int(const std::string &label, return old_value; } +glm::ivec2 Gui::drag_int2(const std::string &label, + glm::ivec2 old_value, + float speed, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragInt2(label.c_str(), (int *)&old_value, speed, minimum, maximum); + return old_value; +} + +glm::ivec3 Gui::drag_int3(const std::string &label, + glm::ivec3 old_value, + float speed, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragInt3(label.c_str(), (int *)&old_value, speed, minimum, maximum); + return old_value; +} + +glm::ivec4 Gui::drag_int4(const std::string &label, + glm::ivec4 old_value, + float speed, + int minimum, + int maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragInt4(label.c_str(), (int *)&old_value, speed, minimum, maximum); + return old_value; +} + float Gui::drag_float(const std::string &label, float old_value, float speed, @@ -265,6 +416,45 @@ float Gui::drag_float(const std::string &label, return old_value; } +glm::vec2 Gui::drag_float2(const std::string &label, + glm::vec2 old_value, + float speed, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragFloat2(label.c_str(), (float *)&old_value, speed, minimum, + maximum); + return old_value; +} + +glm::vec3 Gui::drag_float3(const std::string &label, + glm::vec3 old_value, + float speed, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragFloat3(label.c_str(), (float *)&old_value, speed, minimum, + maximum); + return old_value; +} + +glm::vec4 Gui::drag_float4(const std::string &label, + glm::vec4 old_value, + float speed, + float minimum, + float maximum) { + if (!initialized()) { + return old_value; + } + ImGui::DragFloat4(label.c_str(), (float *)&old_value, speed, minimum, + maximum); + return old_value; +} + bool Gui::tree_node_push(const std::string &label) { if (!initialized()) { return false; diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index c7dba85cd94b7..860b1eeca9d89 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -42,28 +42,94 @@ class TI_DLL_EXPORT Gui final : public GuiBase { int old_value, int minimum, int maximum) override; + glm::ivec2 slider_int2(const std::string &name, + glm::ivec2 old_value, + int minimum, + int maximum) override; + glm::ivec3 slider_int3(const std::string &name, + glm::ivec3 old_value, + int minimum, + int maximum) override; + glm::ivec4 slider_int4(const std::string &name, + glm::ivec4 old_value, + int minimum, + int maximum) override; float slider_float(const std::string &name, float old_value, float minimum, float maximum) override; - // TODO: consider renaming this? + glm::vec2 slider_float2(const std::string &name, + glm::vec2 old_value, + float minimum, + float maximum) override; + glm::vec3 slider_float3(const std::string &name, + glm::vec3 old_value, + float minimum, + float maximum) override; + glm::vec4 slider_float4(const std::string &name, + glm::vec4 old_value, + float minimum, + float maximum) override; glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) override; + glm::vec4 color_edit_4(const std::string &name, glm::vec4 old_value) override; bool button(const std::string &text) override; int combo(const std::string &label, int current_item, const std::vector &items) override; int input_int(const std::string &label, int old_value) override; + glm::ivec2 input_int2(const std::string &label, + glm::ivec2 old_value) override; + glm::ivec3 input_int3(const std::string &label, + glm::ivec3 old_value) override; + glm::ivec4 input_int4(const std::string &label, + glm::ivec4 old_value) override; float input_float(const std::string &label, float old_value) override; + glm::vec2 input_float2(const std::string &label, + glm::vec2 old_value) override; + glm::vec3 input_float3(const std::string &label, + glm::vec3 old_value) override; + glm::vec4 input_float4(const std::string &label, + glm::vec4 old_value) override; int drag_int(const std::string &label, int old_value, float speed, int minimum, int maximum) override; + glm::ivec2 drag_int2(const std::string &label, + glm::ivec2 old_value, + float speed, + int minimum, + int maximum) override; + glm::ivec3 drag_int3(const std::string &label, + glm::ivec3 old_value, + float speed, + int minimum, + int maximum) override; + glm::ivec4 drag_int4(const std::string &label, + glm::ivec4 old_value, + float speed, + int minimum, + int maximum) override; float drag_float(const std::string &label, float old_value, float speed, float minimum, float maximum) override; + glm::vec2 drag_float2(const std::string &label, + glm::vec2 old_value, + float speed, + float minimum, + float maximum) override; + glm::vec3 drag_float3(const std::string &label, + glm::vec3 old_value, + float speed, + float minimum, + float maximum) override; + glm::vec4 drag_float4(const std::string &label, + glm::vec4 old_value, + float speed, + float minimum, + float maximum) override; bool tree_node_push(const std::string &label) override; void tree_node_pop() override; void separator() override; diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index 657a5478bbc99..5b4f62c75de14 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -36,28 +36,94 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { int old_value, int minimum, int maximum) override; + glm::ivec2 slider_int2(const std::string &name, + glm::ivec2 old_value, + int minimum, + int maximum) override; + glm::ivec3 slider_int3(const std::string &name, + glm::ivec3 old_value, + int minimum, + int maximum) override; + glm::ivec4 slider_int4(const std::string &name, + glm::ivec4 old_value, + int minimum, + int maximum) override; float slider_float(const std::string &name, float old_value, float minimum, float maximum) override; - // TODO: consider renaming this? + glm::vec2 slider_float2(const std::string &name, + glm::vec2 old_value, + float minimum, + float maximum) override; + glm::vec3 slider_float3(const std::string &name, + glm::vec3 old_value, + float minimum, + float maximum) override; + glm::vec4 slider_float4(const std::string &name, + glm::vec4 old_value, + float minimum, + float maximum) override; glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) override; + glm::vec4 color_edit_4(const std::string &name, glm::vec4 old_value) override; bool button(const std::string &text) override; int combo(const std::string &label, int current_item, const std::vector &items) override; int input_int(const std::string &label, int old_value) override; + glm::ivec2 input_int2(const std::string &label, + glm::ivec2 old_value) override; + glm::ivec3 input_int3(const std::string &label, + glm::ivec3 old_value) override; + glm::ivec4 input_int4(const std::string &label, + glm::ivec4 old_value) override; float input_float(const std::string &label, float old_value) override; + glm::vec2 input_float2(const std::string &label, + glm::vec2 old_value) override; + glm::vec3 input_float3(const std::string &label, + glm::vec3 old_value) override; + glm::vec4 input_float4(const std::string &label, + glm::vec4 old_value) override; int drag_int(const std::string &label, int old_value, float speed, int minimum, int maximum) override; + glm::ivec2 drag_int2(const std::string &label, + glm::ivec2 old_value, + float speed, + int minimum, + int maximum) override; + glm::ivec3 drag_int3(const std::string &label, + glm::ivec3 old_value, + float speed, + int minimum, + int maximum) override; + glm::ivec4 drag_int4(const std::string &label, + glm::ivec4 old_value, + float speed, + int minimum, + int maximum) override; float drag_float(const std::string &label, float old_value, float speed, float minimum, float maximum) override; + glm::vec2 drag_float2(const std::string &label, + glm::vec2 old_value, + float speed, + float minimum, + float maximum) override; + glm::vec3 drag_float3(const std::string &label, + glm::vec3 old_value, + float speed, + float minimum, + float maximum) override; + glm::vec4 drag_float4(const std::string &label, + glm::vec4 old_value, + float speed, + float minimum, + float maximum) override; bool tree_node_push(const std::string &label) override; void tree_node_pop() override; void separator() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index b1c2187a2104b..3a452d8d35d42 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -78,15 +78,49 @@ ImGui::SliderInt(name.c_str(), &old_value, minimum, maximum); return old_value; } +glm::ivec2 GuiMetal::slider_int2(const std::string &name, glm::ivec2 old_value, + int minimum, int maximum) { + ImGui::SliderInt2(name.c_str(), (int *)&old_value, minimum, maximum); + return old_value; +} +glm::ivec3 GuiMetal::slider_int3(const std::string &name, glm::ivec3 old_value, + int minimum, int maximum) { + ImGui::SliderInt3(name.c_str(), (int *)&old_value, minimum, maximum); + return old_value; +} +glm::ivec4 GuiMetal::slider_int4(const std::string &name, glm::ivec4 old_value, + int minimum, int maximum) { + ImGui::SliderInt4(name.c_str(), (int *)&old_value, minimum, maximum); + return old_value; +} float GuiMetal::slider_float(const std::string &name, float old_value, float minimum, float maximum) { ImGui::SliderFloat(name.c_str(), &old_value, minimum, maximum); return old_value; } +glm::vec2 GuiMetal::slider_float2(const std::string &name, glm::vec2 old_value, + float minimum, float maximum) { + ImGui::SliderFloat2(name.c_str(), (float *)&old_value, minimum, maximum); + return old_value; +} +glm::vec3 GuiMetal::slider_float3(const std::string &name, glm::vec3 old_value, + float minimum, float maximum) { + ImGui::SliderFloat3(name.c_str(), (float *)&old_value, minimum, maximum); + return old_value; +} +glm::vec4 GuiMetal::slider_float4(const std::string &name, glm::vec4 old_value, + float minimum, float maximum) { + ImGui::SliderFloat4(name.c_str(), (float *)&old_value, minimum, maximum); + return old_value; +} glm::vec3 GuiMetal::color_edit_3(const std::string &name, glm::vec3 old_value) { ImGui::ColorEdit3(name.c_str(), (float *)&old_value); return old_value; } +glm::vec4 GuiMetal::color_edit_4(const std::string &name, glm::vec4 old_value) { + ImGui::ColorEdit4(name.c_str(), (float *)&old_value); + return old_value; +} bool GuiMetal::button(const std::string &text) { return ImGui::Button(text.c_str()); } @@ -103,23 +137,98 @@ return old_value; } +glm::ivec2 GuiMetal::input_int2(const std::string &label, + glm::ivec2 old_value) { + ImGui::InputInt2(label.c_str(), (int *)&old_value); + return old_value; +} + +glm::ivec3 GuiMetal::input_int3(const std::string &label, + glm::ivec3 old_value) { + ImGui::InputInt3(label.c_str(), (int *)&old_value); + return old_value; +} + +glm::ivec4 GuiMetal::input_int4(const std::string &label, + glm::ivec4 old_value) { + ImGui::InputInt4(label.c_str(), (int *)&old_value); + return old_value; +} + float GuiMetal::input_float(const std::string &label, float old_value) { ImGui::InputFloat(label.c_str(), &old_value); return old_value; } +glm::vec2 GuiMetal::input_float2(const std::string &label, + glm::vec2 old_value) { + ImGui::InputFloat2(label.c_str(), (float *)&old_value); + return old_value; +} + +glm::vec3 GuiMetal::input_float3(const std::string &label, + glm::vec3 old_value) { + ImGui::InputFloat3(label.c_str(), (float *)&old_value); + return old_value; +} + +glm::vec4 GuiMetal::input_float4(const std::string &label, + glm::vec4 old_value) { + ImGui::InputFloat4(label.c_str(), (float *)&old_value); + return old_value; +} + int GuiMetal::drag_int(const std::string &label, int old_value, float speed, int minimum, int maximum) { ImGui::DragInt(label.c_str(), &old_value, speed, minimum, maximum); return old_value; } +glm::ivec2 GuiMetal::drag_int2(const std::string &label, glm::ivec2 old_value, + float speed, int minimum, int maximum) { + ImGui::DragInt2(label.c_str(), (int *)&old_value, speed, minimum, maximum); + return old_value; +} + +glm::ivec3 GuiMetal::drag_int3(const std::string &label, glm::ivec3 old_value, + float speed, int minimum, int maximum) { + ImGui::DragInt3(label.c_str(), (int *)&old_value, speed, minimum, maximum); + return old_value; +} + +glm::ivec4 GuiMetal::drag_int4(const std::string &label, glm::ivec4 old_value, + float speed, int minimum, int maximum) { + ImGui::DragInt4(label.c_str(), (int *)&old_value, speed, minimum, maximum); + return old_value; +} + float GuiMetal::drag_float(const std::string &label, float old_value, float speed, float minimum, float maximum) { ImGui::DragFloat(label.c_str(), &old_value, speed, minimum, maximum); return old_value; } +glm::vec2 GuiMetal::drag_float2(const std::string &label, glm::vec2 old_value, + float speed, float minimum, float maximum) { + ImGui::DragFloat2(label.c_str(), (float *)&old_value, speed, minimum, + maximum); + return old_value; +} + +glm::vec3 GuiMetal::drag_float3(const std::string &label, glm::vec3 old_value, + float speed, float minimum, float maximum) { + ImGui::DragFloat3(label.c_str(), (float *)&old_value, speed, minimum, + maximum); + return old_value; +} + +glm::vec4 GuiMetal::drag_float4(const std::string &label, glm::vec4 old_value, + float speed, float minimum, float maximum) { + ImGui::DragFloat4(label.c_str(), (float *)&old_value, speed, minimum, + maximum); + return old_value; +} + bool GuiMetal::tree_node_push(const std::string &label) { return ImGui::TreeNode(label.c_str()); } From 947e14ff036586771ba6e7d79ccfed35a6d317a5 Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Mon, 2 Feb 2026 00:16:44 -0700 Subject: [PATCH 7/9] color picker imgui binding --- python/taichi/ui/imgui.py | 20 ++++++++++++++++++++ taichi/python/export_ggui.cpp | 12 ++++++++++++ taichi/ui/common/gui_base.h | 4 ++++ taichi/ui/ggui/gui.cpp | 14 ++++++++++++++ taichi/ui/ggui/gui.h | 4 ++++ taichi/ui/ggui/gui_metal.h | 4 ++++ taichi/ui/ggui/gui_metal.mm | 10 ++++++++++ 7 files changed, 68 insertions(+) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index e0e1863ea3a1b..1896e48e56222 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -158,6 +158,26 @@ def color_edit_3(self, text, old_value): """ return self.gui.color_edit_3(text, old_value) + def color_picker(self, text, old_value): + """Declares a full color picker widget with color wheel/square. + + Auto-detects RGB vs RGBA based on tuple size. + + Args: + text (str): a line of text to be shown next to the picker. + old_value (tuple): the current color value. Can be a tuple of + 3 floats (RGB) or 4 floats (RGBA) in [0,1]. + + Returns: + tuple: the updated color (same size as input). + """ + n = len(old_value) + if n == 3: + return self.gui.color_picker_3(text, tuple(old_value)) + if n == 4: + return self.gui.color_picker_4(text, tuple(old_value)) + raise ValueError(f"color_picker expects 3 (RGB) or 4 (RGBA) components, got {n}") + def button(self, text): """Declares a button, and returns whether or not it had just been clicked. diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index 0b60446ae4f85..e616dcc27fc6b 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -197,6 +197,16 @@ struct PyGui { glm::vec4 new_color = gui->color_edit_4(name, old_color); return vec4_to_tuple(new_color); } + py::tuple color_picker_3(std::string name, py::tuple old_value) { + glm::vec3 old_color = tuple_to_vec3(old_value); + glm::vec3 new_color = gui->color_picker_3(name, old_color); + return vec3_to_tuple(new_color); + } + py::tuple color_picker_4(std::string name, py::tuple old_value) { + glm::vec4 old_color = tuple_to_vec4(old_value); + glm::vec4 new_color = gui->color_picker_4(name, old_color); + return vec4_to_tuple(new_color); + } bool button(std::string name) { return gui->button(name); } @@ -1005,6 +1015,8 @@ void export_ggui(py::module &m) { .def("slider_float4", &PyGui::slider_float4) .def("color_edit_3", &PyGui::color_edit_3) .def("color_edit_4", &PyGui::color_edit_4) + .def("color_picker_3", &PyGui::color_picker_3) + .def("color_picker_4", &PyGui::color_picker_4) .def("button", &PyGui::button) .def("input_int", &PyGui::input_int) .def("input_int2", &PyGui::input_int2) diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index 540884373b4e7..3939ae7b306d5 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -52,6 +52,10 @@ class GuiBase { glm::vec3 old_value) = 0; virtual glm::vec4 color_edit_4(const std::string &name, glm::vec4 old_value) = 0; + virtual glm::vec3 color_picker_3(const std::string &name, + glm::vec3 old_value) = 0; + virtual glm::vec4 color_picker_4(const std::string &name, + glm::vec4 old_value) = 0; virtual bool button(const std::string &text) = 0; virtual int combo(const std::string &label, int current_item, diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index add771ba7da7d..8e53c26583958 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -274,6 +274,20 @@ glm::vec4 Gui::color_edit_4(const std::string &name, glm::vec4 old_value) { ImGui::ColorEdit4(name.c_str(), (float *)&old_value); return old_value; } +glm::vec3 Gui::color_picker_3(const std::string &name, glm::vec3 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::ColorPicker3(name.c_str(), (float *)&old_value); + return old_value; +} +glm::vec4 Gui::color_picker_4(const std::string &name, glm::vec4 old_value) { + if (!initialized()) { + return old_value; + } + ImGui::ColorPicker4(name.c_str(), (float *)&old_value); + return old_value; +} bool Gui::button(const std::string &text) { if (!initialized()) { return false; diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index 860b1eeca9d89..6beb63d3e80e9 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -72,6 +72,10 @@ class TI_DLL_EXPORT Gui final : public GuiBase { float maximum) override; glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) override; glm::vec4 color_edit_4(const std::string &name, glm::vec4 old_value) override; + glm::vec3 color_picker_3(const std::string &name, + glm::vec3 old_value) override; + glm::vec4 color_picker_4(const std::string &name, + glm::vec4 old_value) override; bool button(const std::string &text) override; int combo(const std::string &label, int current_item, diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index 5b4f62c75de14..b2160505e4874 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -66,6 +66,10 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { float maximum) override; glm::vec3 color_edit_3(const std::string &name, glm::vec3 old_value) override; glm::vec4 color_edit_4(const std::string &name, glm::vec4 old_value) override; + glm::vec3 color_picker_3(const std::string &name, + glm::vec3 old_value) override; + glm::vec4 color_picker_4(const std::string &name, + glm::vec4 old_value) override; bool button(const std::string &text) override; int combo(const std::string &label, int current_item, diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index 3a452d8d35d42..c752aa3c36cfc 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -121,6 +121,16 @@ ImGui::ColorEdit4(name.c_str(), (float *)&old_value); return old_value; } +glm::vec3 GuiMetal::color_picker_3(const std::string &name, + glm::vec3 old_value) { + ImGui::ColorPicker3(name.c_str(), (float *)&old_value); + return old_value; +} +glm::vec4 GuiMetal::color_picker_4(const std::string &name, + glm::vec4 old_value) { + ImGui::ColorPicker4(name.c_str(), (float *)&old_value); + return old_value; +} bool GuiMetal::button(const std::string &text) { return ImGui::Button(text.c_str()); } From 6ac128d4967c0f14a0d7c6d9dd57ba27627d9139 Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Mon, 2 Feb 2026 00:20:06 -0700 Subject: [PATCH 8/9] dedup string caching code --- taichi/python/export_ggui.cpp | 73 +++++++++++++++-------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index e616dcc27fc6b..b907c742359af 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -104,15 +104,37 @@ py::array_t mat4_to_nparray(glm::mat4 mat) { struct PyGui { GuiBase *gui = nullptr; // not owned - // Cache for combo items: label -> (tuple identity, strings, cstr_ptrs) + // Cache for string list items (combo, listbox): label -> cached data // Frame-based cleanup removes entries not used since last frame - struct ComboCache { + struct StringListCache { py::tuple items_tuple; // for identity comparison std::vector items_str; // owns the string data std::vector items_cstr; // points into items_str bool touched = false; // used this frame? }; - std::unordered_map combo_cache_; + std::unordered_map string_list_cache_; + + // Get cached C string pointers for a tuple of Python strings. + // Rebuilds cache if tuple identity changed; marks entry as touched. + const std::vector &get_cached_strings_(const std::string &label, + py::tuple items_py) { + auto it = string_list_cache_.find(label); + if (it == string_list_cache_.end() || + !it->second.items_tuple.is(items_py)) { + StringListCache cache; + cache.items_tuple = items_py; + for (auto item : items_py) { + cache.items_str.push_back(item.cast()); + } + for (const auto &s : cache.items_str) { + cache.items_cstr.push_back(s.c_str()); + } + string_list_cache_[label] = std::move(cache); + it = string_list_cache_.find(label); + } + it->second.touched = true; + return it->second.items_cstr; + } void begin(std::string name, float x, @@ -339,55 +361,24 @@ struct PyGui { gui->end_tab_item(); } int combo(std::string label, int current_item, py::tuple items_py) { - auto it = combo_cache_.find(label); - - // Cache hit if same label and same tuple identity - if (it == combo_cache_.end() || !it->second.items_tuple.is(items_py)) { - // Build new cache entry - ComboCache cache; - cache.items_tuple = items_py; - for (auto item : items_py) { - cache.items_str.push_back(item.cast()); - } - for (const auto &s : cache.items_str) { - cache.items_cstr.push_back(s.c_str()); - } - combo_cache_[label] = std::move(cache); - it = combo_cache_.find(label); - } - it->second.touched = true; - return gui->combo(label, current_item, it->second.items_cstr); + const auto &items = get_cached_strings_(label, items_py); + return gui->combo(label, current_item, items); } int listbox(std::string label, int current_item, py::tuple items_py, int height_in_items) { - // Use same cache as combo (keyed by label) - auto it = combo_cache_.find(label); - - if (it == combo_cache_.end() || !it->second.items_tuple.is(items_py)) { - ComboCache cache; - cache.items_tuple = items_py; - for (auto item : items_py) { - cache.items_str.push_back(item.cast()); - } - for (const auto &s : cache.items_str) { - cache.items_cstr.push_back(s.c_str()); - } - combo_cache_[label] = std::move(cache); - it = combo_cache_.find(label); - } - it->second.touched = true; - return gui->listbox(label, current_item, it->second.items_cstr, - height_in_items); + const auto &items = get_cached_strings_(label, items_py); + return gui->listbox(label, current_item, items, height_in_items); } // Called at frame end to clean up stale cache entries void frame_end() { - for (auto it = combo_cache_.begin(); it != combo_cache_.end();) { + for (auto it = string_list_cache_.begin(); + it != string_list_cache_.end();) { if (!it->second.touched) { - it = combo_cache_.erase(it); + it = string_list_cache_.erase(it); } else { it->second.touched = false; ++it; From 8409a999071e7c90aa259f8116177b2a38b6800c Mon Sep 17 00:00:00 2001 From: Isaac Nygaard Date: Mon, 2 Feb 2026 00:31:35 -0700 Subject: [PATCH 9/9] basic table imgui binding --- python/taichi/ui/imgui.py | 85 +++++++++++++++++++++++++++++++++++ taichi/python/export_ggui.cpp | 37 ++++++++++----- taichi/ui/common/gui_base.h | 6 +++ taichi/ui/ggui/gui.cpp | 42 +++++++++++++++++ taichi/ui/ggui/gui.h | 6 +++ taichi/ui/ggui/gui_metal.h | 6 +++ taichi/ui/ggui/gui_metal.mm | 16 +++++++ 7 files changed, 188 insertions(+), 10 deletions(-) diff --git a/python/taichi/ui/imgui.py b/python/taichi/ui/imgui.py index 1896e48e56222..13a0acf266f7f 100644 --- a/python/taichi/ui/imgui.py +++ b/python/taichi/ui/imgui.py @@ -547,3 +547,88 @@ def tab_item(self, label): yield finally: self.gui.end_tab_item() + + def begin_table(self, table_id, columns): + """Begin a table (low-level). + + Args: + table_id (str): Unique identifier for the table. + columns (int): Number of columns. + + Returns: + bool: True if the table is visible and should be rendered. + + Note: + Must call end_table() if this returns True. + Consider using table() generator instead for automatic cleanup. + """ + return self.gui.begin_table(table_id, columns) + + def end_table(self): + """End a table (low-level). + + Only call this if begin_table() returned True. + """ + self.gui.end_table() + + def table_setup_column(self, label): + """Set up a column header for the table. + + Call after begin_table() and before the first row. + + Args: + label (str): Label for the column header. + """ + self.gui.table_setup_column(label) + + def table_headers_row(self): + """Submit the header row after setting up columns. + + Call after all table_setup_column() calls and before data rows. + """ + self.gui.table_headers_row() + + def table_next_row(self): + """Begin a new row in the table.""" + self.gui.table_next_row() + + def table_next_column(self): + """Move to the next column in the current row. + + Returns: + bool: True if the column is visible (not clipped). + """ + return self.gui.table_next_column() + + def table(self, table_id, columns): + """Table container (generator). + + Use with a for loop - the body runs only if the table is visible, + and end_table is called automatically. + + Args: + table_id (str): Unique identifier for the table. + columns (int): Number of columns. + + Yields: + Nothing, but loop body executes only if table is visible. + + Example:: + + for _ in gui.table("my_table", 3): + gui.table_setup_column("ID") + gui.table_setup_column("Name") + gui.table_setup_column("Price") + gui.table_headers_row() + + for item in items: + gui.table_next_row() + gui.table_next_column(); gui.text(str(item["id"])) + gui.table_next_column(); gui.text(item["name"]) + gui.table_next_column(); gui.text(f"${item['price']:.2f}") + """ + if self.gui.begin_table(table_id, columns): + try: + yield + finally: + self.gui.end_table() diff --git a/taichi/python/export_ggui.cpp b/taichi/python/export_ggui.cpp index b907c742359af..0a51ced19bef7 100644 --- a/taichi/python/export_ggui.cpp +++ b/taichi/python/export_ggui.cpp @@ -136,15 +136,8 @@ struct PyGui { return it->second.items_cstr; } - void begin(std::string name, - float x, - float y, - float width, - float height, - bool movable = true, - bool resizable = true, - bool collapsible = true) { - gui->begin(name, x, y, width, height, movable, resizable, collapsible); + void begin(std::string name, float x, float y, float width, float height) { + gui->begin(name, x, y, width, height); } void end() { gui->end(); @@ -360,6 +353,24 @@ struct PyGui { void end_tab_item() { gui->end_tab_item(); } + bool begin_table(std::string id, int columns) { + return gui->begin_table(id, columns); + } + void end_table() { + gui->end_table(); + } + void table_setup_column(std::string label) { + gui->table_setup_column(label); + } + void table_headers_row() { + gui->table_headers_row(); + } + void table_next_row() { + gui->table_next_row(); + } + bool table_next_column() { + return gui->table_next_column(); + } int combo(std::string label, int current_item, py::tuple items_py) { const auto &items = get_cached_strings_(label, items_py); return gui->combo(label, current_item, items); @@ -1040,7 +1051,13 @@ void export_ggui(py::module &m) { .def("begin_tab_bar", &PyGui::begin_tab_bar) .def("end_tab_bar", &PyGui::end_tab_bar) .def("begin_tab_item", &PyGui::begin_tab_item) - .def("end_tab_item", &PyGui::end_tab_item); + .def("end_tab_item", &PyGui::end_tab_item) + .def("begin_table", &PyGui::begin_table) + .def("end_table", &PyGui::end_table) + .def("table_setup_column", &PyGui::table_setup_column) + .def("table_headers_row", &PyGui::table_headers_row) + .def("table_next_row", &PyGui::table_next_row) + .def("table_next_column", &PyGui::table_next_column); py::class_(m, "PyScene") .def(py::init<>()) diff --git a/taichi/ui/common/gui_base.h b/taichi/ui/common/gui_base.h index 3939ae7b306d5..f98144159aab1 100644 --- a/taichi/ui/common/gui_base.h +++ b/taichi/ui/common/gui_base.h @@ -132,6 +132,12 @@ class GuiBase { virtual void end_tab_bar() = 0; virtual bool begin_tab_item(const std::string &label) = 0; virtual void end_tab_item() = 0; + virtual bool begin_table(const std::string &id, int columns) = 0; + virtual void end_table() = 0; + virtual void table_setup_column(const std::string &label) = 0; + virtual void table_headers_row() = 0; + virtual void table_next_row() = 0; + virtual bool table_next_column() = 0; virtual void prepare_for_next_frame() = 0; virtual ~GuiBase() = default; }; diff --git a/taichi/ui/ggui/gui.cpp b/taichi/ui/ggui/gui.cpp index 8e53c26583958..25f41b9c0026d 100644 --- a/taichi/ui/ggui/gui.cpp +++ b/taichi/ui/ggui/gui.cpp @@ -580,6 +580,48 @@ void Gui::end_tab_item() { ImGui::EndTabItem(); } +bool Gui::begin_table(const std::string &id, int columns) { + if (!initialized()) { + return false; + } + return ImGui::BeginTable(id.c_str(), columns); +} + +void Gui::end_table() { + if (!initialized()) { + return; + } + ImGui::EndTable(); +} + +void Gui::table_setup_column(const std::string &label) { + if (!initialized()) { + return; + } + ImGui::TableSetupColumn(label.c_str()); +} + +void Gui::table_headers_row() { + if (!initialized()) { + return; + } + ImGui::TableHeadersRow(); +} + +void Gui::table_next_row() { + if (!initialized()) { + return; + } + ImGui::TableNextRow(); +} + +bool Gui::table_next_column() { + if (!initialized()) { + return false; + } + return ImGui::TableNextColumn(); +} + void Gui::draw(taichi::lang::CommandList *cmd_list) { // Rendering ImGui::Render(); diff --git a/taichi/ui/ggui/gui.h b/taichi/ui/ggui/gui.h index 6beb63d3e80e9..7b588f8882080 100644 --- a/taichi/ui/ggui/gui.h +++ b/taichi/ui/ggui/gui.h @@ -152,6 +152,12 @@ class TI_DLL_EXPORT Gui final : public GuiBase { void end_tab_bar() override; bool begin_tab_item(const std::string &label) override; void end_tab_item() override; + bool begin_table(const std::string &id, int columns) override; + void end_table() override; + void table_setup_column(const std::string &label) override; + void table_headers_row() override; + void table_next_row() override; + bool table_next_column() override; void draw(taichi::lang::CommandList *cmd_list); diff --git a/taichi/ui/ggui/gui_metal.h b/taichi/ui/ggui/gui_metal.h index b2160505e4874..35b5ba4f637eb 100644 --- a/taichi/ui/ggui/gui_metal.h +++ b/taichi/ui/ggui/gui_metal.h @@ -146,6 +146,12 @@ class TI_DLL_EXPORT GuiMetal final : public GuiBase { void end_tab_bar() override; bool begin_tab_item(const std::string &label) override; void end_tab_item() override; + bool begin_table(const std::string &id, int columns) override; + void end_table() override; + void table_setup_column(const std::string &label) override; + void table_headers_row() override; + void table_next_row() override; + bool table_next_column() override; void prepare_for_next_frame() override; diff --git a/taichi/ui/ggui/gui_metal.mm b/taichi/ui/ggui/gui_metal.mm index c752aa3c36cfc..d39d95bc71755 100644 --- a/taichi/ui/ggui/gui_metal.mm +++ b/taichi/ui/ggui/gui_metal.mm @@ -288,6 +288,22 @@ void GuiMetal::end_tab_item() { ImGui::EndTabItem(); } +bool GuiMetal::begin_table(const std::string &id, int columns) { + return ImGui::BeginTable(id.c_str(), columns); +} + +void GuiMetal::end_table() { ImGui::EndTable(); } + +void GuiMetal::table_setup_column(const std::string &label) { + ImGui::TableSetupColumn(label.c_str()); +} + +void GuiMetal::table_headers_row() { ImGui::TableHeadersRow(); } + +void GuiMetal::table_next_row() { ImGui::TableNextRow(); } + +bool GuiMetal::table_next_column() { return ImGui::TableNextColumn(); } + void GuiMetal::draw(taichi::lang::CommandList *cmd_list) { ImGui_ImplMetal_NewFrame(current_rpd_);