diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a7a9d4..1aa8914 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ pkg_check_modules(DEPS REQUIRED pixman-1 libdrm) add_library(hy3 SHARED src/main.cpp src/Hy3Layout.cpp + src/SelectionHook.cpp ) target_include_directories(hy3 PRIVATE diff --git a/README.md b/README.md index 99c0c12..45331b8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ i3 / sway like layout for [hyprland](https://github.com/hyprwm/hyprland). - [x] Window splits - [x] Window movement - [x] Window resizing -- [ ] Selecting a group of windows at once (and related movement) +- [x] Selecting a group of windows at once (and related movement) - [ ] Tabbed groups - [ ] Some convenience dispatchers not found in i3 or sway @@ -35,6 +35,7 @@ You can use `hy3:makegroup` to create a new split. - `hy3:makegroup, ` - make a vertical or horizontal split - `hy3:movefocus, ` - move the focus left, up, down, or right - `hy3:movewindow, ` - move a window left, up, down, or right + - `hy3:raisefocus` - raise the active focus one level - `hy3:debugnodes` - print the node tree into the hyprland log ## Installing diff --git a/src/Hy3Layout.cpp b/src/Hy3Layout.cpp index 9e286fc..682d527 100644 --- a/src/Hy3Layout.cpp +++ b/src/Hy3Layout.cpp @@ -1,5 +1,6 @@ #include "globals.hpp" #include "Hy3Layout.hpp" +#include "SelectionHook.hpp" #include @@ -197,6 +198,69 @@ void Hy3Node::recalcSizePosRecursive(bool force) { } } +void Hy3Node::markFocused() { + Hy3Node* node = this; + + // undo decos for root focus + auto* root = node; + while (root->parent != nullptr) root = root->parent; + auto* oldfocus = root->getFocusedNode(); + + // update focus + if (this->data.type == Hy3NodeData::Group) { + this->data.as_group.lastFocusedChild = nullptr; + } + + while (node->parent != nullptr) { + node->parent->data.as_group.lastFocusedChild = node; + node = node->parent; + } + + if (oldfocus != nullptr) { + oldfocus->updateDecos(); + } +} + +void Hy3Node::focus() { + this->markFocused(); + + switch (this->data.type) { + case Hy3NodeData::Window: + g_pCompositor->focusWindow(this->data.as_window); + break; + case Hy3NodeData::Group: + g_pCompositor->focusWindow(nullptr); + this->raiseToTop(); + break; + } +} + +void Hy3Node::raiseToTop() { + switch (this->data.type) { + case Hy3NodeData::Window: + g_pCompositor->moveWindowToTop(this->data.as_window); + break; + case Hy3NodeData::Group: + for (auto* child: this->data.as_group.children) { + child->raiseToTop(); + } + break; + } +} + +Hy3Node* Hy3Node::getFocusedNode() { + switch (this->data.type) { + case Hy3NodeData::Window: + return this; + case Hy3NodeData::Group: + if (this->data.as_group.lastFocusedChild == nullptr) { + return this; + } else { + return this->data.as_group.lastFocusedChild->getFocusedNode(); + } + } +} + bool Hy3Node::swallowGroups(Hy3Node* into) { if (into == nullptr || into->data.type != Hy3NodeData::Group @@ -291,6 +355,18 @@ void Hy3Node::swapData(Hy3Node& a, Hy3Node& b) { } } +void Hy3Node::updateDecos() { + switch (this->data.type) { + case Hy3NodeData::Window: + g_pCompositor->updateWindowAnimatedDecorationValues(this->data.as_window); + break; + case Hy3NodeData::Group: + for (auto* child: this->data.as_group.children) { + child->updateDecos(); + } + } +} + int Hy3Layout::getWorkspaceNodeCount(const int& id) { int count = 0; @@ -496,7 +572,7 @@ void Hy3Layout::onWindowCreatedTiling(CWindow* window) { } Debug::log(LOG, "opened new window %p(node: %p) on window %p in %p", window, &node, opening_after, opening_into); - opening_into->data.as_group.lastFocusedChild = &node; + node.markFocused(); opening_into->recalcSizePosRecursive(); Debug::log(LOG, "opening_into (%p) contains new child (%p)? %d", opening_into, &node, opening_into->data.as_group.hasChild(&node)); } @@ -542,25 +618,23 @@ void Hy3Layout::onWindowRemovedTiling(CWindow* window) { CWindow* Hy3Layout::getNextWindowCandidate(CWindow* window) { auto* node = this->getWorkspaceRootGroup(window->m_iWorkspaceID); if (node == nullptr) return nullptr; - while (node->data.type == Hy3NodeData::Group) { - node = node->data.as_group.lastFocusedChild; - if (node == nullptr) { - Debug::log(ERR, "A group's last focused child was null when getting the next selection candidate"); - return nullptr; - } + + node = node->getFocusedNode(); + + switch (node->data.type) { + case Hy3NodeData::Window: + return node->data.as_window; + case Hy3NodeData::Group: + return nullptr; } - return node->data.as_window; } void Hy3Layout::onWindowFocusChange(CWindow* window) { - Debug::log(LOG, "Switched windows from to %p", window); + Debug::log(LOG, "Switched windows to %p", window); auto* node = this->getNodeFromWindow(window); if (node == nullptr) return; - while (node->parent != nullptr) { - node->parent->data.as_group.lastFocusedChild = node; - node = node->parent; - } + node->markFocused(); } bool Hy3Layout::isWindowTiled(CWindow* window) { @@ -921,14 +995,17 @@ void Hy3Layout::onEnable() { this->onWindowCreatedTiling(window.get()); } + + setup_selection_hook(); } void Hy3Layout::onDisable() { + disable_selection_hook(); this->nodes.clear(); } -void Hy3Layout::makeGroupOn(CWindow* window, Hy3GroupLayout layout) { - auto* node = this->getNodeFromWindow(window); +void Hy3Layout::makeGroupOn(int workspace, Hy3GroupLayout layout) { + auto* node = this->getWorkspaceRootGroup(workspace)->getFocusedNode(); if (node == nullptr) return; if (node->parent->data.as_group.children.size() == 1 @@ -942,7 +1019,7 @@ void Hy3Layout::makeGroupOn(CWindow* window, Hy3GroupLayout layout) { this->nodes.push_back({ .parent = node, - .data = node->data.as_window, + .data = node->data, .workspace_id = node->workspace_id, .layout = this, }); @@ -957,9 +1034,11 @@ void Hy3Layout::makeGroupOn(CWindow* window, Hy3GroupLayout layout) { Hy3Node* shiftOrGetFocus(Hy3Node& node, ShiftDirection direction, bool shift); -void Hy3Layout::shiftFocus(CWindow* window, ShiftDirection direction) { - Debug::log(LOG, "ShiftFocus %p %d", window, direction); - auto* node = this->getNodeFromWindow(window); +void Hy3Layout::shiftFocus(int workspace, ShiftDirection direction) { + auto* root = this->getWorkspaceRootGroup(workspace); + if (root == nullptr) return; + auto* node = root->getFocusedNode(); + Debug::log(LOG, "ShiftFocus %p %d", node, direction); if (node == nullptr) return; Hy3Node* target; @@ -968,9 +1047,11 @@ void Hy3Layout::shiftFocus(CWindow* window, ShiftDirection direction) { } } -void Hy3Layout::shiftWindow(CWindow* window, ShiftDirection direction) { - Debug::log(LOG, "ShiftWindow %p %d", window, direction); - auto* node = this->getNodeFromWindow(window); +void Hy3Layout::shiftWindow(int workspace, ShiftDirection direction) { + auto* root = this->getWorkspaceRootGroup(workspace); + if (root == nullptr) return; + auto* node = root->getFocusedNode(); + Debug::log(LOG, "ShiftWindow %p %d", node, direction); if (node == nullptr) return; @@ -1131,12 +1212,7 @@ Hy3Node* Hy3Layout::shiftOrGetFocus(Hy3Node& node, ShiftDirection direction, boo target_parent = target_parent->parent; } - auto* focusnode = &node; - - while (focusnode->parent != nullptr) { - focusnode->parent->data.as_group.lastFocusedChild = focusnode; - focusnode = focusnode->parent; - } + node.markFocused(); if (target_parent != target_group && target_parent != nullptr) target_parent->recalcSizePosRecursive(); @@ -1145,6 +1221,34 @@ Hy3Node* Hy3Layout::shiftOrGetFocus(Hy3Node& node, ShiftDirection direction, boo return nullptr; } +void Hy3Layout::raiseFocus(int workspace) { + auto* root = this->getWorkspaceRootGroup(workspace); + if (root == nullptr) return; + auto* node = root->getFocusedNode(); + + if (node->parent != nullptr && node->parent->parent != nullptr) { + node->parent->focus(); + node->parent->updateDecos(); + } +} + +bool Hy3Layout::shouldRenderSelected(CWindow* window) { + if (window == nullptr) return false; + auto* root = this->getWorkspaceRootGroup(window->m_iWorkspaceID); + if (root == nullptr || root->data.as_group.lastFocusedChild == nullptr) return false; + auto* focused = root->getFocusedNode(); + if (focused == nullptr) return false; + + switch (focused->data.type) { + case Hy3NodeData::Window: + return false; + case Hy3NodeData::Group: + auto* node = this->getNodeFromWindow(window); + if (node == nullptr) return false; + return focused->data.as_group.hasChild(node); + } +} + std::string Hy3Node::debugNode() { std::stringstream buf; std::string addr = "0x" + std::to_string((size_t)this); diff --git a/src/Hy3Layout.hpp b/src/Hy3Layout.hpp index a4d3384..fdeef7f 100644 --- a/src/Hy3Layout.hpp +++ b/src/Hy3Layout.hpp @@ -71,6 +71,11 @@ struct Hy3Node { void recalcSizePosRecursive(bool force = false); std::string debugNode(); + void markFocused(); + void focus(); + void raiseToTop(); + Hy3Node* getFocusedNode(); + void updateDecos(); bool operator==(const Hy3Node&) const; @@ -105,9 +110,12 @@ public: virtual void onEnable(); virtual void onDisable(); - void makeGroupOn(CWindow*, Hy3GroupLayout); - void shiftWindow(CWindow*, ShiftDirection); - void shiftFocus(CWindow*, ShiftDirection); + void makeGroupOn(int, Hy3GroupLayout); + void shiftWindow(int, ShiftDirection); + void shiftFocus(int, ShiftDirection); + void raiseFocus(int); + + bool shouldRenderSelected(CWindow*); Hy3Node* getWorkspaceRootGroup(const int&); diff --git a/src/SelectionHook.cpp b/src/SelectionHook.cpp new file mode 100644 index 0000000..c1b93b8 --- /dev/null +++ b/src/SelectionHook.cpp @@ -0,0 +1,43 @@ +#include "globals.hpp" +#include "src/Window.hpp" +#include +#include + +inline CFunctionHook* g_LastSelectionHook = nullptr; + +void hook_update_decos(void* thisptr, CWindow* window) { + bool explicitly_selected = g_Hy3Layout->shouldRenderSelected(window); + Debug::log(LOG, "update decos for %p - selected: %d", window, explicitly_selected); + + auto* lastWindow = g_pCompositor->m_pLastWindow; + if (explicitly_selected) { + g_pCompositor->m_pLastWindow = window; + } + + ((void (*)(void*, CWindow*)) g_LastSelectionHook->m_pOriginal)(thisptr, window); + + if (explicitly_selected) { + g_pCompositor->m_pLastWindow = lastWindow; + } +} + +void setup_selection_hook() { + if (g_LastSelectionHook == nullptr) { + static const auto decoUpdateCandidates = HyprlandAPI::findFunctionsByName(PHANDLE, "updateWindowAnimatedDecorationValues"); + + if (decoUpdateCandidates.size() != 1) { + Debug::log(ERR, "Expected one matching function to hook for \"updateWindowAnimatedDecorationValues\", found %d", decoUpdateCandidates.size()); + return; + } + + g_LastSelectionHook = HyprlandAPI::createFunctionHook(PHANDLE, decoUpdateCandidates[0].address, (void *)&hook_update_decos); + } + + g_LastSelectionHook->hook(); +} + +void disable_selection_hook() { + if (g_LastSelectionHook != nullptr) { + g_LastSelectionHook->unhook(); + } +} diff --git a/src/SelectionHook.hpp b/src/SelectionHook.hpp new file mode 100644 index 0000000..f15fd23 --- /dev/null +++ b/src/SelectionHook.hpp @@ -0,0 +1,4 @@ +#pragma once + +void setup_selection_hook(); +void disable_selection_hook(); diff --git a/src/globals.hpp b/src/globals.hpp index 8aac0ed..6d16586 100644 --- a/src/globals.hpp +++ b/src/globals.hpp @@ -1,3 +1,5 @@ #include +#include "Hy3Layout.hpp" inline HANDLE PHANDLE = nullptr; +inline std::unique_ptr g_Hy3Layout; diff --git a/src/main.cpp b/src/main.cpp index 5c8aa08..2126376 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,8 @@ #include +#include #include "globals.hpp" -#include "Hy3Layout.hpp" -#include "src/Compositor.hpp" -#include "src/config/ConfigManager.hpp" -inline std::unique_ptr g_Hy3Layout; APICALL EXPORT std::string PLUGIN_API_VERSION() { return HYPRLAND_API_VERSION; @@ -25,53 +22,73 @@ CWindow* window_for_action() { return window; } +int workspace_for_action() { + if (g_pLayoutManager->getCurrentLayout() != g_Hy3Layout.get()) return -1; + + int workspace_id = g_pCompositor->m_pLastMonitor->activeWorkspace; + + if (workspace_id < 0) return -1; + auto* workspace = g_pCompositor->getWorkspaceByID(workspace_id); + if (workspace == nullptr) return -1; + if (workspace->m_bHasFullscreenWindow) return -1; + + return workspace_id; +} + void dispatch_makegroup(std::string arg) { - CWindow* window = window_for_action(); - if (window == nullptr) return; + int workspace = workspace_for_action(); + if (workspace < 0) return; if (arg == "h") { - g_Hy3Layout->makeGroupOn(window, Hy3GroupLayout::SplitH); + g_Hy3Layout->makeGroupOn(workspace, Hy3GroupLayout::SplitH); } else if (arg == "v") { - g_Hy3Layout->makeGroupOn(window, Hy3GroupLayout::SplitV); + g_Hy3Layout->makeGroupOn(workspace, Hy3GroupLayout::SplitV); } } void dispatch_movewindow(std::string arg) { - CWindow* window = window_for_action(); - if (window == nullptr) return; + int workspace = workspace_for_action(); + if (workspace < 0) return; if (arg == "l") { - g_Hy3Layout->shiftWindow(window, ShiftDirection::Left); + g_Hy3Layout->shiftWindow(workspace, ShiftDirection::Left); } else if (arg == "u") { - g_Hy3Layout->shiftWindow(window, ShiftDirection::Up); + g_Hy3Layout->shiftWindow(workspace, ShiftDirection::Up); } else if (arg == "d") { - g_Hy3Layout->shiftWindow(window, ShiftDirection::Down); + g_Hy3Layout->shiftWindow(workspace, ShiftDirection::Down); } else if (arg == "r") { - g_Hy3Layout->shiftWindow(window, ShiftDirection::Right); + g_Hy3Layout->shiftWindow(workspace, ShiftDirection::Right); } } void dispatch_movefocus(std::string arg) { - CWindow* window = window_for_action(); - if (window == nullptr) return; + int workspace = workspace_for_action(); + if (workspace < 0) return; if (arg == "l") { - g_Hy3Layout->shiftFocus(window, ShiftDirection::Left); + g_Hy3Layout->shiftFocus(workspace, ShiftDirection::Left); } else if (arg == "u") { - g_Hy3Layout->shiftFocus(window, ShiftDirection::Up); + g_Hy3Layout->shiftFocus(workspace, ShiftDirection::Up); } else if (arg == "d") { - g_Hy3Layout->shiftFocus(window, ShiftDirection::Down); + g_Hy3Layout->shiftFocus(workspace, ShiftDirection::Down); } else if (arg == "r") { - g_Hy3Layout->shiftFocus(window, ShiftDirection::Right); + g_Hy3Layout->shiftFocus(workspace, ShiftDirection::Right); } } -void dispatch_debug(std::string arg) { - CWindow* window = window_for_action(); - if (window == nullptr) return; +void dispatch_raisefocus(std::string arg) { + int workspace = workspace_for_action(); + if (workspace < 0) return; - auto* root = g_Hy3Layout->getWorkspaceRootGroup(window->m_iWorkspaceID); - if (window == nullptr) { + g_Hy3Layout->raiseFocus(workspace); +} + +void dispatch_debug(std::string arg) { + int workspace = workspace_for_action(); + if (workspace < 0) return; + + auto* root = g_Hy3Layout->getWorkspaceRootGroup(workspace); + if (workspace < 0) { Debug::log(LOG, "DEBUG NODES: no nodes on workspace"); } else { Debug::log(LOG, "DEBUG NODES\n%s", root->debugNode().c_str()); @@ -89,6 +106,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addDispatcher(PHANDLE, "hy3:makegroup", dispatch_makegroup); HyprlandAPI::addDispatcher(PHANDLE, "hy3:movefocus", dispatch_movefocus); HyprlandAPI::addDispatcher(PHANDLE, "hy3:movewindow", dispatch_movewindow); + HyprlandAPI::addDispatcher(PHANDLE, "hy3:raisefocus", dispatch_raisefocus); HyprlandAPI::addDispatcher(PHANDLE, "hy3:debugnodes", dispatch_debug); return {"hy3", "i3 like layout for hyprland", "outfoxxed", "0.1"};