From 032c52d84374e29177011e248a219a21a2944aa2 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Sun, 21 Dec 2025 17:40:58 +0300 Subject: [PATCH] Added drag to move code to tree listing --- .../NetworkingObjects/NetObjOrderedList.hpp | 13 + src/Bookmarks/BookmarkManager.cpp | 15 +- src/GUIStuff/Elements/TreeListing.cpp | 231 ++++++++++++++---- src/GUIStuff/Elements/TreeListing.hpp | 33 ++- 4 files changed, 243 insertions(+), 49 deletions(-) diff --git a/include/Helpers/NetworkingObjects/NetObjOrderedList.hpp b/include/Helpers/NetworkingObjects/NetObjOrderedList.hpp index 4e9903b..8b9f173 100644 --- a/include/Helpers/NetworkingObjects/NetObjOrderedList.hpp +++ b/include/Helpers/NetworkingObjects/NetObjOrderedList.hpp @@ -100,6 +100,7 @@ namespace NetworkingObjects { virtual const std::vector>& get_data() const = 0; virtual uint32_t size() const = 0; virtual const NetObjOrderedListObjectInfoPtr& at(uint32_t index) const = 0; + virtual NetObjOrderedListObjectInfoPtr get(const NetObjID& id) const = 0; virtual bool empty() const = 0; virtual ~NetObjOrderedList() {} protected: @@ -172,6 +173,12 @@ namespace NetworkingObjects { virtual const NetObjOrderedListObjectInfoPtr& at(uint32_t index) const override { return data[index]; } + virtual NetObjOrderedListObjectInfoPtr get(const NetObjID& id) const override { + auto it = idToDataMap.find(id); + if(it == idToDataMap.end()) + return nullptr; + return it->second; + } virtual bool empty() const override { return data.empty(); } @@ -343,6 +350,12 @@ namespace NetworkingObjects { virtual const NetObjOrderedListObjectInfoPtr& at(uint32_t index) const override { return clientData[index]; } + virtual NetObjOrderedListObjectInfoPtr get(const NetObjID& id) const override { + auto it = clientIdToDataMap.find(id); + if(it == clientIdToDataMap.end()) + return nullptr; + return it->second; + } virtual bool empty() const override { return clientData.empty(); } diff --git a/src/Bookmarks/BookmarkManager.cpp b/src/Bookmarks/BookmarkManager.cpp index 9370bd8..0a4cce2 100644 --- a/src/Bookmarks/BookmarkManager.cpp +++ b/src/Bookmarks/BookmarkManager.cpp @@ -70,6 +70,9 @@ void BookmarkManager::setup_list_gui(const std::string& id) { .isDirectoryOpen = bookmarkParentFolder.at(index)->obj->is_folder() ? bookmarkParentFolder.at(index)->obj->is_folder_open() : false }; }, + .getIndexOfObjInList = [&](const GUIStuff::TreeListing::ParentObjectIDPair& idPair) { + return world.netObjMan.get_obj_temporary_ref_from_id(idPair.parent)->get_folder_list()->get(idPair.object)->pos; + }, .setDirectoryOpen = [&](NetObjID netID, bool newDirectoryOpen) { world.netObjMan.get_obj_temporary_ref_from_id(netID)->set_folder_open(newDirectoryOpen); }, @@ -84,20 +87,24 @@ void BookmarkManager::setup_list_gui(const std::string& id) { } return false; }, - .drawObjGUI = [&](NetObjID parentNetID, NetObjID netID) { + .drawObjGUI = [&](const GUIStuff::TreeListing::ParentObjectIDPair& idPair) { CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, .childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER} }, }) { - gui.text_label(world.netObjMan.get_obj_temporary_ref_from_id(netID)->get_name()); + gui.text_label(world.netObjMan.get_obj_temporary_ref_from_id(idPair.object)->get_name()); } if(gui.svg_icon_button_transparent("delete button", "data/icons/trash.svg", false, GUIStuff::TreeListing::ENTRY_HEIGHT)) { - toDeleteParent = parentNetID; - toDeleteObject = netID; + toDeleteParent = idPair.parent; + toDeleteObject = idPair.object; } return false; + }, + .moveObjectsToListAtIndex = [](NetworkingObjects::NetObjID listObj, size_t index, const std::vector& objsToInsert) { + static int i = 0; + std::cout << "Move happened! " << i++ << std::endl; } }); if(toDeleteObject.has_value()) { diff --git a/src/GUIStuff/Elements/TreeListing.cpp b/src/GUIStuff/Elements/TreeListing.cpp index 8c2218a..4fedc61 100644 --- a/src/GUIStuff/Elements/TreeListing.cpp +++ b/src/GUIStuff/Elements/TreeListing.cpp @@ -5,49 +5,129 @@ namespace GUIStuff { void TreeListing::update(UpdateInputData& io, GUIManager& gui, NetworkingObjects::NetObjID rootObjID, const DisplayData& displayData) { + if(!io.mouse.leftHeld) + dragHoldSelected = false; + + orderedObjsSelected.clear(); + std::optional hoveredObject; + CLAY_AUTO_ID({ - .layout = { - .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)} - } + .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}} }) { - gui.scroll_bar_area("scroll area", true, [&](float scrollContentHeight, float containerHeight, float& scrollAmount) { - size_t itemCount = 0; - size_t itemCountToStartAt = -scrollAmount / ENTRY_HEIGHT; - size_t itemCountToEndAt = (containerHeight - scrollAmount) / ENTRY_HEIGHT; - itemCountToEndAt++; - CLAY_AUTO_ID({ - .layout = { - .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT * itemCountToStartAt)} - } - }) {} - recursive_gui(io, gui, displayData, rootObjID, itemCount, itemCountToStartAt, itemCountToEndAt, 0); - if(itemCount > itemCountToEndAt) { + Clay_ElementId localID = CLAY_ID_LOCAL("scroll area"); + float setScrollAmount; + CLAY(localID, { + .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}} + }) { + gui.scroll_bar_area("scroll area", true, [&](float scrollContentHeight, float containerHeight, float& scrollAmount) { + setScrollAmount = scrollAmount; + size_t itemCount = 0; + size_t itemCountToStartAt = -scrollAmount / ENTRY_HEIGHT; + size_t itemCountToEndAt = (containerHeight - scrollAmount) / ENTRY_HEIGHT; + itemCountToEndAt++; + std::vector parentIDStack; CLAY_AUTO_ID({ .layout = { - .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT * (itemCount - itemCountToEndAt))} + .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT * itemCountToStartAt)} } }) {} - } - }); + recursive_gui(io, gui, displayData, rootObjID, itemCount, itemCountToStartAt, itemCountToEndAt, 0, hoveredObject, parentIDStack, false); + if(itemCount > itemCountToEndAt) { + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT * (itemCount - itemCountToEndAt))} + } + }) {} + } + }); + } + Clay_ElementData scrollAreaBB = Clay_GetElementData(localID); + if(scrollAreaBB.found) { + float mouseRelativeYPos = std::fabs(io.mouse.pos.y() - scrollAreaBB.boundingBox.y - setScrollAmount); + topHalfOfHovered = std::fmod(mouseRelativeYPos, ENTRY_HEIGHT) < (ENTRY_HEIGHT / 2.0f); + } } + + // Any objects that were part of a collapsed directory will be unselected + objsSelected.clear(); + for(auto& [parent, object] : orderedObjsSelected) + objsSelected.emplace(object); + + if(objDragged.has_value()) { + ParentObjectIDPair& objDraggedVal = objDragged.value(); + if(!objsSelected.contains(objDraggedVal.object)) + objDragged = std::nullopt; + else if(!io.mouse.leftHeld) { + if(!hoveredObject.has_value()) + set_single_selected_object(); + else { + ParentObjectIDStack& hoveredObjVal = hoveredObject.value(); + size_t indexToMoveFrom = displayData.getIndexOfObjInList(objDraggedVal); + size_t indexToMoveTo = displayData.getIndexOfObjInList({hoveredObjVal.parents.back(), hoveredObjVal.object}); + if(!topHalfOfHovered) + indexToMoveTo++; + if((indexToMoveFrom == indexToMoveTo || (indexToMoveFrom + 1) == indexToMoveTo) && objDraggedVal.parent == hoveredObjVal.parents.back()) + set_single_selected_object(); + else { + bool wrongMovement = false; + // Check if object is about to move into itself + for(const NetworkingObjects::NetObjID& parent : hoveredObjVal.parents) { + if(objsSelected.contains(parent)) { + wrongMovement = true; + break; + } + } + if(!wrongMovement) + displayData.moveObjectsToListAtIndex(hoveredObjVal.parents.back(), indexToMoveTo, orderedObjsSelected); + objsSelected.clear(); + orderedObjsSelected.clear(); + } + } + objDragged = std::nullopt; + } + } + + oldHoveredObject = hoveredObject; } -void TreeListing::recursive_gui(UpdateInputData& io, GUIManager& gui, const DisplayData& displayData, NetworkingObjects::NetObjID objID, size_t& itemCount, size_t itemCountToStartAt, size_t itemCountToEndAt, size_t listDepth) { +void TreeListing::set_single_selected_object() { + objsSelected.clear(); + orderedObjsSelected.clear(); + objsSelected.emplace(objDragged.value().object); + orderedObjsSelected.emplace_back(objDragged.value()); +} + +void TreeListing::recursive_gui(UpdateInputData& io, GUIManager& gui, const DisplayData& displayData, NetworkingObjects::NetObjID objID, size_t& itemCount, size_t itemCountToStartAt, size_t itemCountToEndAt, size_t listDepth, std::optional& hoveredObject, std::vector& parentIDStack, bool parentJustSelected) { + parentIDStack.emplace_back(objID); for(size_t i = 0;; i++) { std::optional objInListOptional = displayData.getObjInListAtIndex(objID, i); if(!objInListOptional.has_value()) break; else { auto& objInList = objInListOptional.value(); + bool selfJustSelected = false; if(itemCount >= itemCountToStartAt && itemCount < itemCountToEndAt) { gui.push_id(itemCount); bool buttonClicked = false; + uint16_t topOutlineWidth = 0; + uint16_t bottomOutlineWidth = 0; + if(oldHoveredObject.has_value() && oldHoveredObject.value().object == objInList.id) { + if(objDragged.has_value()) { + ParentObjectIDPair& objDraggedVal = objDragged.value(); + if(objDraggedVal.object != objInList.id) { + if(topHalfOfHovered) + topOutlineWidth = 1; + else + bottomOutlineWidth = 1; + } + } + } CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT)}, .childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER} }, - .backgroundColor = convert_vec4((objSelected.has_value() && objSelected.value() == objInList.id) ? io.theme->backColor1 : io.theme->backColor2), + .backgroundColor = convert_vec4((objsSelected.contains(objInList.id)) ? io.theme->backColor1 : io.theme->backColor2) }) { if(listDepth != 0) { CLAY_AUTO_ID({ @@ -56,41 +136,108 @@ void TreeListing::recursive_gui(UpdateInputData& io, GUIManager& gui, const Disp }, }) {} } - if(objInList.isDirectory) { - if(gui.svg_icon_button_transparent("ico", objInList.isDirectoryOpen ? "data/icons/droparrow.svg" : "data/icons/droparrowclose.svg", false, ENTRY_HEIGHT)) { - displayData.setDirectoryOpen(objInList.id, !objInList.isDirectoryOpen); - buttonClicked = true; - } - } - else { - CLAY_AUTO_ID({ - .layout = { - .sizing = {.width = CLAY_SIZING_FIXED(ENTRY_HEIGHT), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT)}, - .childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER} - }, - }) { - buttonClicked |= displayData.drawNonDirectoryObjIconGUI(objInList.id); - } - } CLAY_AUTO_ID({ .layout = { - .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, - .childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER} + .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)} + }, + .border = { + .color = convert_vec4(io.theme->frontColor1), + .width = { + .left = 0, + .right = 0, + .top = topOutlineWidth, + .bottom = bottomOutlineWidth, + .betweenChildren = 0 + } } }) { - buttonClicked |= displayData.drawObjGUI(objID, objInList.id); + if(objInList.isDirectory) { + if(gui.svg_icon_button_transparent("ico", objInList.isDirectoryOpen ? "data/icons/droparrow.svg" : "data/icons/droparrowclose.svg", false, ENTRY_HEIGHT)) { + displayData.setDirectoryOpen(objInList.id, !objInList.isDirectoryOpen); + buttonClicked = true; + } + } + else { + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_FIXED(ENTRY_HEIGHT), .height = CLAY_SIZING_FIXED(ENTRY_HEIGHT)}, + .childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER} + }, + }) { + buttonClicked |= displayData.drawNonDirectoryObjIconGUI(objInList.id); + } + } + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, + .childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER} + } + }) { + buttonClicked |= displayData.drawObjGUI({objID, objInList.id}); + } } - if(Clay_Hovered() && io.mouse.leftClick && !buttonClicked) - objSelected = objInList.id; + if(Clay_Hovered()) { + hoveredObject = {parentIDStack, objInList.id}; + if(io.mouse.leftClick && !buttonClicked && !parentJustSelected) { + if(io.key.leftShift) + dragHoldSelected = true; + else if(io.key.leftCtrl) { + bool alreadySelected = objsSelected.contains(objInList.id); + if(alreadySelected) + objsSelected.erase(objInList.id); + else { + select_and_unselect_parents(parentIDStack, objInList.id); + selfJustSelected = true; + } + } + else { + if(!objsSelected.contains(objInList.id)) { + objsSelected.clear(); + orderedObjsSelected.clear(); + objsSelected.emplace(objInList.id); + selfJustSelected = true; + } + objDragged = {objID, objInList.id}; + } + } + if(io.key.leftShift && dragHoldSelected && !parentJustSelected) { + select_and_unselect_parents(parentIDStack, objInList.id); + selfJustSelected = true; + } + } } gui.pop_id(); } + if(!parentJustSelected) { + if(objsSelected.contains(objInList.id)) + orderedObjsSelected.emplace_back(objID, objInList.id); + } + else + unselect_object(objInList.id); itemCount++; if(objInList.isDirectoryOpen) - recursive_gui(io, gui, displayData, objInList.id, itemCount, itemCountToStartAt, itemCountToEndAt, listDepth + 1); + recursive_gui(io, gui, displayData, objInList.id, itemCount, itemCountToStartAt, itemCountToEndAt, listDepth + 1, hoveredObject, parentIDStack, selfJustSelected); } } + parentIDStack.pop_back(); +} + +void TreeListing::unselect_object(NetworkingObjects::NetObjID id) { + objsSelected.erase(id); + std::erase_if(orderedObjsSelected, [&id](const auto& p) { + return p.object == id; + }); +} + +void TreeListing::select_and_unselect_parents(const std::vector& parentIDStack, NetworkingObjects::NetObjID newSelectedObj) { + for(const NetworkingObjects::NetObjID& parent : parentIDStack) { + if(objsSelected.contains(parent)) { + unselect_object(parent); + break; + } + } + objsSelected.emplace(newSelectedObj); } void TreeListing::clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) { diff --git a/src/GUIStuff/Elements/TreeListing.hpp b/src/GUIStuff/Elements/TreeListing.hpp index fcffbe3..53bc520 100644 --- a/src/GUIStuff/Elements/TreeListing.hpp +++ b/src/GUIStuff/Elements/TreeListing.hpp @@ -9,6 +9,18 @@ class GUIManager; class TreeListing : public Element { public: static constexpr float ENTRY_HEIGHT = 25.0f; + + struct ParentObjectIDPair { + NetworkingObjects::NetObjID parent; + NetworkingObjects::NetObjID object; + bool operator==(const ParentObjectIDPair&) const = default; + }; + + struct ParentObjectIDStack { + std::vector parents; + NetworkingObjects::NetObjID object; + }; + struct DisplayData { struct ObjInList { NetworkingObjects::NetObjID id; @@ -16,15 +28,30 @@ class TreeListing : public Element { bool isDirectoryOpen = false; }; std::function(NetworkingObjects::NetObjID, size_t)> getObjInListAtIndex; + std::function getIndexOfObjInList; std::function setDirectoryOpen; std::function drawNonDirectoryObjIconGUI; - std::function drawObjGUI; + std::function drawObjGUI; + std::function&)> moveObjectsToListAtIndex; }; void update(UpdateInputData& io, GUIManager& gui, NetworkingObjects::NetObjID rootObjID, const DisplayData& displayData); virtual void clay_draw(SkCanvas* canvas, UpdateInputData& io, Clay_RenderCommand* command) override; private: - void recursive_gui(UpdateInputData& io, GUIManager& gui, const DisplayData& displayData, NetworkingObjects::NetObjID objID, size_t& itemCount, size_t itemCountToStartAt, size_t itemCountToEndAt, size_t listDepth); - std::optional objSelected; + bool dragHoldSelected = false; + + void set_single_selected_object(); + + void recursive_gui(UpdateInputData& io, GUIManager& gui, const DisplayData& displayData, NetworkingObjects::NetObjID objID, size_t& itemCount, size_t itemCountToStartAt, size_t itemCountToEndAt, size_t listDepth, std::optional& hoveredObject, std::vector& parentIDStack, bool parentJustSelected); + std::vector orderedObjsSelected; + std::unordered_set objsSelected; + + void select_and_unselect_parents(const std::vector& parentIDStack, NetworkingObjects::NetObjID newSelectedObj); + + void unselect_object(NetworkingObjects::NetObjID id); + + bool topHalfOfHovered = false; + std::optional objDragged; + std::optional oldHoveredObject; }; }