diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a0dfe0..ef0e0a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ add_library(${PROJECT_NAME} SHARED # Window source files src/windows/GraphWindow.cpp src/windows/ModuleManagerWindow.cpp + src/windows/NodeExplorerWindow.cpp # Widget source files src/widgets/InputField.cpp diff --git a/include/flow/ui/Window.hpp b/include/flow/ui/Window.hpp index 11e4e79..c82528e 100644 --- a/include/flow/ui/Window.hpp +++ b/include/flow/ui/Window.hpp @@ -10,7 +10,8 @@ FLOW_UI_NAMESPACE_START /// Default name of the main dockspace. -static inline const std::string DefaultDockspace = "MainDockSpace"; +static inline const std::string DefaultDockspace = "MainDockSpace"; +static inline const std::string PropertyDockspace = "PropertySpace"; /** * @brief Base Windows class. diff --git a/include/flow/ui/windows/GraphWindow.hpp b/include/flow/ui/windows/GraphWindow.hpp index 3f87707..94aa17b 100644 --- a/include/flow/ui/windows/GraphWindow.hpp +++ b/include/flow/ui/windows/GraphWindow.hpp @@ -4,6 +4,7 @@ #pragma once #include "flow/ui/Core.hpp" +#include "flow/ui/Widget.hpp" #include "flow/ui/Window.hpp" #include "flow/ui/views/NodeView.hpp" @@ -18,6 +19,27 @@ FLOW_UI_NAMESPACE_START +class ContextMenu : public Widget +{ + public: + ContextMenu(std::shared_ptr factory) : _factory(std::move(factory)) {} + + virtual ~ContextMenu() = default; + + virtual void operator()() noexcept override; + + private: + void DrawPopupCategory(const std::string& category, const flow::CategoryMap& registered_nodes); + + public: + Event OnSelection; + + private: + std::shared_ptr _factory; + std::string node_lookup; + bool is_focused = false; +}; + /** * @brief Graph editor window for creating flows. */ @@ -180,15 +202,12 @@ class GraphWindow : public Window void CreateItems(); void CleanupDeadItems(); - void ShowNodeContextMenu(); void OnLoadNode(const flow::SharedNode& node, const json& position_json); void OnLoadConnection(const flow::SharedConnection& connection); flow::SharedNode CreateNode(const std::string& class_name, const std::string& display_name); - void DrawPopupCategory(const std::string& category, const flow::CategoryMap& registered_nodes); - private: std::unique_ptr _editor_ctx; std::shared_ptr _graph; @@ -204,6 +223,8 @@ class GraphWindow : public Window std::string node_lookup; + ContextMenu node_context_menu; + struct { float x = 0.f; diff --git a/include/flow/ui/windows/NodeExplorerWindow.hpp b/include/flow/ui/windows/NodeExplorerWindow.hpp new file mode 100644 index 0000000..1a815d7 --- /dev/null +++ b/include/flow/ui/windows/NodeExplorerWindow.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "Core.hpp" +#include "Window.hpp" + +#include +#include +#include +#include + +FLOW_UI_NAMESPACE_START + +class NodeExplorerWindow : public Window +{ + public: + NodeExplorerWindow(std::shared_ptr env); + virtual ~NodeExplorerWindow() = default; + + virtual void Draw() override; + + void SetActiveGraph(std::shared_ptr graph) { _active_graph = std::move(graph); } + + private: + void DrawPopupCategory(const std::string& category, const flow::CategoryMap& registered_nodes); + + private: + std::shared_ptr _env; + std::shared_ptr _active_graph; + std::string node_lookup; + struct + { + std::string class_name; + std::string display_name; + } drag_drop_payload; +}; + +FLOW_UI_NAMESPACE_END diff --git a/src/Editor.cpp b/src/Editor.cpp index ff2b02a..174be06 100644 --- a/src/Editor.cpp +++ b/src/Editor.cpp @@ -9,6 +9,7 @@ #include "Window.hpp" #include "utilities/Conversions.hpp" #include "windows/ModuleManagerWindow.hpp" +#include "windows/NodeExplorerWindow.hpp" #include #include @@ -146,7 +147,14 @@ void Editor::Init(const std::string& initial_file) _factory->RegisterInputType(std::chrono::months::zero()); _factory->RegisterInputType(std::chrono::years::zero()); - AddWindow(std::make_shared(_env, default_modules_path), DefaultDockspace); + auto node_explorer = std::make_shared(_env); + OnActiveGraphChanged.Bind("NodeExplorer", [window = node_explorer](const auto& g) { window->SetActiveGraph(g); }); + + AddDockspace(PropertyDockspace, DefaultDockspace, 0.25f, DockspaceSplitDirection::Left); + AddDockspace("PropertySubSpace", PropertyDockspace, 0.5f, DockspaceSplitDirection::Down); + + AddWindow(std::move(node_explorer), "PropertySubSpace"); + AddWindow(std::make_shared(_env, default_modules_path), "PropertySubSpace"); if (!initial_file.empty()) { diff --git a/src/windows/GraphWindow.cpp b/src/windows/GraphWindow.cpp index 4c7963a..72a80fd 100644 --- a/src/windows/GraphWindow.cpp +++ b/src/windows/GraphWindow.cpp @@ -29,6 +29,109 @@ FLOW_UI_NAMESPACE_START using namespace ax; namespace ed = ax::NodeEditor; +void ContextMenu::operator()() noexcept +{ + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5, 5)); + ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(15, 15, 15, 240)); + + ImGui::SetNextWindowSizeConstraints(ImVec2(300, 300), ImVec2(315, 400)); + if (!ImGui::BeginPopup("Create New Node", ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking)) + { + is_focused = false; + node_lookup = ""; + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + return; + } + + if (node_lookup.empty() && !is_focused) + { + ImGui::SetKeyboardFocusHere(0); + is_focused = true; + } + + ImGui::BeginHorizontal("search"); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 20.f); + ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(36, 36, 36, 255)); + + ImGui::SetNextItemAllowOverlap(); + ImGui::InputText("##Search", &node_lookup, 0); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + ImGui::SetCursorPosX((ImGui::GetItemRectMax() - ImGui::GetItemRectMin()).x - 18); + + ImGui::PushFont(std::bit_cast(GetConfig().IconFont.get())); + ImGui::TextUnformatted(ICON_FA_MAGNIFYING_GLASS); + ImGui::PopFont(); + + ImGui::EndHorizontal(); + + auto registered_nodes = _factory->GetCategories(); + + if (!node_lookup.empty()) + { + auto partial_match_func = [&](const auto& entry) -> bool { + auto [_, class_name] = entry; + std::string display_name = _factory->GetFriendlyName(class_name); + std::string filter = node_lookup; + + const auto& to_lower = [](unsigned char c) { return static_cast(std::tolower(c)); }; + std::transform(filter.begin(), filter.end(), filter.begin(), to_lower); + std::transform(class_name.begin(), class_name.end(), class_name.begin(), to_lower); + std::transform(display_name.begin(), display_name.end(), display_name.begin(), to_lower); + + return std::string_view(display_name).find(filter) == std::string_view::npos && + std::string_view(class_name).find(filter) == std::string_view::npos; + }; + + std::erase_if(registered_nodes, partial_match_func); + } + + if (ImGui::BeginChild("Categories")) + { + std::set categories; + for (const auto& [category, _] : registered_nodes) + { + categories.insert(category); + } + + for (const auto& category : categories) + { + DrawPopupCategory(category, registered_nodes); + } + + ImGui::EndChild(); + } + + ImGui::EndPopup(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); +} + +void ContextMenu::DrawPopupCategory(const std::string& category, const flow::CategoryMap& registered_nodes) +{ + if (!ImGui::TreeNodeEx(category.c_str(), node_lookup.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen)) return; + + auto [begin_it, end_it] = registered_nodes.equal_range(category); + for (auto it = begin_it; it != end_it; ++it) + { + auto class_name = it->second; + auto display_name = _factory->GetFriendlyName(class_name); + + ImGui::Bullet(); + if (ImGui::MenuItem(display_name.c_str())) + { + OnSelection(class_name, display_name); + break; + } + } + ImGui::TreePop(); +} + namespace { inline void DrawLabel(const char* label, ImColor color) @@ -101,7 +204,8 @@ constexpr GraphWindow::ActionType operator&(const GraphWindow::ActionType& a, co return static_cast(static_cast(a) & static_cast(b)); } -GraphWindow::GraphWindow(std::shared_ptr graph) : Window(graph->GetName()), _graph{std::move(graph)} +GraphWindow::GraphWindow(std::shared_ptr graph) + : Window(graph->GetName()), _graph{std::move(graph)}, node_context_menu{GetEnv()->GetFactory()} { ed::Config config; config.UserPointer = this; @@ -133,6 +237,10 @@ GraphWindow::GraphWindow(std::shared_ptr graph) : Window(graph->Get _editor_ctx = std::unique_ptr(std::bit_cast(ed::CreateEditor(&config))); + auto& canvas_view = const_cast(GetEditorDetailContext(_editor_ctx)->GetView()); + canvas_view.Origin = {0, 0}; + canvas_view.InvScale = 75; + auto& ed_style = GetEditorDetailContext(GetEditorContext())->GetStyle(); ed_style.NodeBorderWidth = 0.5f; ed_style.FlowDuration = 1.f; @@ -146,13 +254,51 @@ GraphWindow::GraphWindow(std::shared_ptr graph) : Window(graph->Get }); _graph->Visit([](const auto& node) { return node->Start(); }); + + node_context_menu.OnSelection = [this, factory = std::dynamic_pointer_cast(GetEnv()->GetFactory())]( + const auto& class_name, const auto& display_name) { + CreateNode(class_name, display_name); + ImGui::CloseCurrentPopup(); + }; + + _graph->OnNodeAdded.Bind("CreateNodeView", [this](const auto& n) { + const auto factory = std::dynamic_pointer_cast(GetEnv()->GetFactory()); + auto node_view = factory->CreateNodeView(n); + _item_views.emplace(node_view->ID(), node_view); + ed::SetNodePosition(node_view->ID(), {_open_popup_position.x, _open_popup_position.y}); + + if (auto start_pin = _new_node_link_pin) + { + auto& pins = start_pin->Kind == PortType::Input ? node_view->Outputs : node_view->Inputs; + for (auto& pin : pins) + { + if (!start_pin->CanLink(pin) && !(factory->IsConvertible(start_pin->Type(), pin->Type()) || + factory->IsConvertible(pin->Type(), start_pin->Type()))) + { + continue; + } + + auto end_pin = pin; + if (start_pin->Kind == PortType::Input) std::swap(start_pin, end_pin); + + const auto& start_node = FindNode(start_pin->NodeID)->Node; + const auto& end_node = FindNode(end_pin->NodeID)->Node; + const auto& conn = + _graph->ConnectNodes(start_node->ID(), start_pin->Name(), end_node->ID(), end_pin->Name()); + + _links.emplace(std::hash{}(conn->ID()), + ConnectionView{conn->ID(), start_pin->ID, end_pin->ID, start_pin->GetColour()}); + break; + } + } + }); } GraphWindow::~GraphWindow() { _graph->Visit([](const auto& node) { return node->Stop(); }); + _graph->Clear(); - _item_views.clear(); _links.clear(); ed::DestroyEditor(std::bit_cast(_editor_ctx.get())); @@ -187,6 +333,23 @@ try auto cursorTopLeft = ImGui::GetCursorScreenPos(); CreateItems(); + + if (ImGui::BeginDragDropTarget()) + { + if (auto payload = ImGui::AcceptDragDropPayload("NewNode")) + { + std::string class_name = reinterpret_cast(payload->Data); + std::string display_name = GetEnv()->GetFactory()->GetFriendlyName(class_name); + + _graph->OnNodeAdded.Bind( + "SetPos", [](auto&& n) { ed::SetNodePosition(std::hash{}(n->ID()), ImGui::GetMousePos()); }); + CreateNode(class_name, display_name); + _graph->OnNodeAdded.Unbind("SetPos"); + } + + ImGui::EndDragDropTarget(); + } + CleanupDeadItems(); for (auto& [_, item] : _item_views) @@ -218,11 +381,8 @@ try } ed::Suspend(); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5, 5)); - ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(15, 15, 15, 240)); - ShowNodeContextMenu(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); + node_context_menu(); + ed::Resume(); if (ImGui::IsWindowFocused() && ed::AreShortcutsEnabled()) @@ -487,7 +647,6 @@ void GraphWindow::CreateItems() if (ed::AcceptNewItem()) { - _create_new_node = true; _new_node_link_pin = FindPort(pinId); _new_link_pin = nullptr; ed::Suspend(); @@ -523,86 +682,6 @@ void GraphWindow::CleanupDeadItems() ed::EndDelete(); } -void GraphWindow::ShowNodeContextMenu() -{ - // FIXME: Bit of a hack. - static bool is_focused = false; - - ImGui::SetNextWindowSizeConstraints(ImVec2(300, 300), ImVec2(315, 400)); - if (!ImGui::BeginPopup("Create New Node", ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking)) - { - _create_new_node = false; - is_focused = false; - node_lookup = ""; - return; - } - - if (node_lookup.empty() && !is_focused) - { - ImGui::SetKeyboardFocusHere(0); - is_focused = true; - } - - ImGui::BeginHorizontal("search"); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 20.f); - ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(36, 36, 36, 255)); - - ImGui::SetNextItemAllowOverlap(); - ImGui::InputText("##Search", &node_lookup, 0); - - ImGui::PopStyleColor(); - ImGui::PopStyleVar(); - - ImGui::SetCursorPos({(ImGui::GetItemRectMax() - ImGui::GetItemRectMin()).x - 18, 4}); - - ImGui::PushFont(std::bit_cast(GetConfig().IconFont.get())); - ImGui::TextUnformatted(ICON_FA_MAGNIFYING_GLASS); - ImGui::PopFont(); - - ImGui::EndHorizontal(); - - const auto factory = GetEnv()->GetFactory(); - auto registered_nodes = factory->GetCategories(); - - if (!node_lookup.empty()) - { - auto partial_match_func = [&](const auto& entry) -> bool { - auto [_, class_name] = entry; - std::string display_name = factory->GetFriendlyName(class_name); - std::string filter = node_lookup; - - const auto& to_lower = [](unsigned char c) { return static_cast(std::tolower(c)); }; - std::transform(filter.begin(), filter.end(), filter.begin(), to_lower); - std::transform(class_name.begin(), class_name.end(), class_name.begin(), to_lower); - std::transform(display_name.begin(), display_name.end(), display_name.begin(), to_lower); - - return std::string_view(display_name).find(filter) == std::string_view::npos && - std::string_view(class_name).find(filter) == std::string_view::npos; - }; - - std::erase_if(registered_nodes, partial_match_func); - } - - if (ImGui::BeginChild("Categories")) - { - std::set categories; - for (const auto& [category, _] : registered_nodes) - { - categories.insert(category); - } - - for (const auto& category : categories) - { - DrawPopupCategory(category, registered_nodes); - } - - ImGui::EndChild(); - } - - ImGui::EndPopup(); -} - void GraphWindow::OnLoadNode(const flow::SharedNode& node, const json& position_json) { auto node_view = FindNode(std::hash{}(node->ID())); @@ -656,58 +735,6 @@ flow::SharedNode GraphWindow::CreateNode(const std::string& class_name, const st return new_node; } -void GraphWindow::DrawPopupCategory(const std::string& category, const flow::CategoryMap& registered_nodes) -{ - if (!ImGui::TreeNodeEx(category.c_str(), node_lookup.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen)) return; - - const auto factory = std::dynamic_pointer_cast(GetEnv()->GetFactory()); - auto [begin_it, end_it] = registered_nodes.equal_range(category); - for (auto it = begin_it; it != end_it; ++it) - { - auto class_name = it->second; - auto display_name = factory->GetFriendlyName(class_name); - - ImGui::Bullet(); - if (ImGui::MenuItem(display_name.c_str())) - { - auto new_node = CreateNode(class_name, display_name); - auto node_view = factory->CreateNodeView(new_node); - _item_views.emplace(node_view->ID(), node_view); - _create_new_node = false; - ImGui::CloseCurrentPopup(); - - ed::SetNodePosition(node_view->ID(), {_open_popup_position.x, _open_popup_position.y}); - - if (auto start_pin = _new_node_link_pin) - { - auto& pins = start_pin->Kind == PortType::Input ? node_view->Outputs : node_view->Inputs; - for (auto& pin : pins) - { - if (!start_pin->CanLink(pin) && (factory->IsConvertible(start_pin->Type(), pin->Type()) || - factory->IsConvertible(pin->Type(), start_pin->Type()))) - { - continue; - } - - auto end_pin = pin; - if (start_pin->Kind == PortType::Input) std::swap(start_pin, end_pin); - - const auto& start_node = FindNode(start_pin->NodeID)->Node; - const auto& end_node = FindNode(end_pin->NodeID)->Node; - const auto& conn = - _graph->ConnectNodes(start_node->ID(), start_pin->Name(), end_node->ID(), end_pin->Name()); - - _links.emplace(std::hash{}(conn->ID()), - ConnectionView{conn->ID(), start_pin->ID, end_pin->ID, start_pin->GetColour()}); - break; - } - } - break; - } - } - ImGui::TreePop(); -} - json GraphWindow::SaveFlow() { json graph_json = *_graph; diff --git a/src/windows/NodeExplorerWindow.cpp b/src/windows/NodeExplorerWindow.cpp new file mode 100644 index 0000000..f4c95d6 --- /dev/null +++ b/src/windows/NodeExplorerWindow.cpp @@ -0,0 +1,111 @@ +#include "NodeExplorerWindow.hpp" + +#include "Config.hpp" + +#include +#include +#include +#include +#include + +FLOW_UI_NAMESPACE_START + +using namespace ax; +namespace ed = ax::NodeEditor; + +NodeExplorerWindow::NodeExplorerWindow(std::shared_ptr env) : Window("Node Selection"), _env(std::move(env)) {} + +void NodeExplorerWindow::Draw() +{ + if (!_active_graph) + { + return Window::Draw(); + } + + ImGui::BeginHorizontal("search"); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 20.f); + ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(36, 36, 36, 255)); + + ImGui::SetNextItemAllowOverlap(); + + ImGui::PushItemWidth(ImGui::GetWindowWidth() - (ImGui::GetStyle().WindowPadding.x * 2)); + ImGui::InputText("##Search", &node_lookup, 0); + ImGui::PopItemWidth(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + ImGui::SetCursorPosX((ImGui::GetItemRectMax() - ImGui::GetItemRectMin()).x - 18); + + ImGui::PushFont(std::bit_cast(GetConfig().IconFont.get())); + ImGui::TextUnformatted(ICON_FA_MAGNIFYING_GLASS); + ImGui::PopFont(); + + ImGui::EndHorizontal(); + + auto factory = _env->GetFactory(); + + auto registered_nodes = factory->GetCategories(); + + if (!node_lookup.empty()) + { + auto partial_match_func = [&](const auto& entry) -> bool { + auto [_, class_name] = entry; + std::string display_name = factory->GetFriendlyName(class_name); + std::string filter = node_lookup; + + const auto& to_lower = [](unsigned char c) { return static_cast(std::tolower(c)); }; + std::transform(filter.begin(), filter.end(), filter.begin(), to_lower); + std::transform(class_name.begin(), class_name.end(), class_name.begin(), to_lower); + std::transform(display_name.begin(), display_name.end(), display_name.begin(), to_lower); + + return std::string_view(display_name).find(filter) == std::string_view::npos && + std::string_view(class_name).find(filter) == std::string_view::npos; + }; + + std::erase_if(registered_nodes, partial_match_func); + } + + if (ImGui::BeginChild("Categories")) + { + std::set categories; + for (const auto& [category, _] : registered_nodes) + { + categories.insert(category); + } + + for (const auto& category : categories) + { + DrawPopupCategory(category, registered_nodes); + } + + ImGui::EndChild(); + } +} + +void NodeExplorerWindow::DrawPopupCategory(const std::string& category, const flow::CategoryMap& registered_nodes) +{ + if (!ImGui::TreeNodeEx(category.c_str(), node_lookup.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen)) return; + + auto [begin_it, end_it] = registered_nodes.equal_range(category); + for (auto it = begin_it; it != end_it; ++it) + { + const auto display_name = _env->GetFactory()->GetFriendlyName(it->second); + + ImGui::Bullet(); + ImGui::Selectable(display_name.c_str()); + + if (ImGui::BeginDragDropSource()) + { + const auto class_name = it->second; + ImGui::Text("+ Create Node"); + ImGui::SetDragDropPayload("NewNode", class_name.c_str(), class_name.size() + 1, ImGuiCond_Once); + ImGui::EndDragDropSource(); + } + } + + ImGui::TreePop(); +} + +FLOW_UI_NAMESPACE_END diff --git a/third_party/flow-core b/third_party/flow-core index c574f0c..460adfa 160000 --- a/third_party/flow-core +++ b/third_party/flow-core @@ -1 +1 @@ -Subproject commit c574f0caa4fcdce83d0016cdc1a824fbe09efd72 +Subproject commit 460adfac699d63c2533441f8e2dc810762e85d23 diff --git a/third_party/hello_imgui b/third_party/hello_imgui index 7009475..7b8657c 160000 --- a/third_party/hello_imgui +++ b/third_party/hello_imgui @@ -1 +1 @@ -Subproject commit 700947573b8d01bcc1946bd15d3e57f85e6258be +Subproject commit 7b8657c4b2c2d57f0a7db6049f181d8718f8720f diff --git a/third_party/imgui b/third_party/imgui index 2b95af1..df2b6d6 160000 --- a/third_party/imgui +++ b/third_party/imgui @@ -1 +1 @@ -Subproject commit 2b95af15fa975da2fe33f751fedb603cf5f599c9 +Subproject commit df2b6d6b68ad0151eb76780fead73fa7053d401d diff --git a/third_party/nfd b/third_party/nfd index 388549a..29e3bcb 160000 --- a/third_party/nfd +++ b/third_party/nfd @@ -1 +1 @@ -Subproject commit 388549a5badaa7cbd138f5f189f50c67d5bf060c +Subproject commit 29e3bcb578345b9fa345d1d7683f00c150565ca3 diff --git a/third_party/spdlog b/third_party/spdlog index 24dde31..96a8f62 160000 --- a/third_party/spdlog +++ b/third_party/spdlog @@ -1 +1 @@ -Subproject commit 24dde318fe034f0c7e809daa5a6e81dc85e1155a +Subproject commit 96a8f6250cbf4e8c76387c614f666710a2fa9bad