diff --git a/CMakeLists.txt b/CMakeLists.txt index 64e3c51..5ad5ce0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,11 +11,12 @@ if(CMAKE_EXPORT_COMPILE_COMMANDS) endif() find_package(PkgConfig REQUIRED) -pkg_check_modules(DEPS REQUIRED hyprland pixman-1 libdrm) +pkg_check_modules(DEPS REQUIRED hyprland pixman-1 libdrm pango pangocairo) add_library(hy3 SHARED src/main.cpp src/Hy3Layout.cpp + src/TabGroup.cpp src/SelectionHook.cpp ) diff --git a/flake.lock b/flake.lock index c869952..2764e8b 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "xdph": "xdph" }, "locked": { - "lastModified": 1682603803, - "narHash": "sha256-NY9nVAdB7UyInu2vPx/DIUVNZ83t4RdP16QY9DTIn4s=", + "lastModified": 1684167111, + "narHash": "sha256-0JKyr8WOpcXJP5XaLnUSI7e1d7N5Rcpyf72+N4ZEtjo=", "owner": "hyprwm", "repo": "Hyprland", - "rev": "f23455e592bca14e0abd9249de467cc71cd2850e", + "rev": "5b84b0fb445bc4485510bba516c84141aaeafd04", "type": "github" }, "original": { @@ -44,11 +44,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1682453498, - "narHash": "sha256-WoWiAd7KZt5Eh6n+qojcivaVpnXKqBsVgpixpV2L9CE=", + "lastModified": 1683014792, + "narHash": "sha256-6Va9iVtmmsw4raBc3QKvQT2KT/NGRWlvUlJj46zN8B8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c8018361fa1d1650ee8d4b96294783cf564e8a7f", + "rev": "1a411f23ba299db155a5b45d5e145b85a7aafc42", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 75e1b45..af84a45 100644 --- a/flake.nix +++ b/flake.nix @@ -15,8 +15,10 @@ nativeBuildInputs = with pkgs; [ cmake pkg-config ]; - buildInputs = [ + buildInputs = with pkgs; [ hyprland.packages.${system}.hyprland.dev + pango + cairo ] ++ hyprland.packages.${system}.hyprland.buildInputs; # no noticeable impact on performance and greatly assists debugging diff --git a/src/Hy3Layout.cpp b/src/Hy3Layout.cpp index 68e04a8..6ec06ec 100644 --- a/src/Hy3Layout.cpp +++ b/src/Hy3Layout.cpp @@ -41,18 +41,6 @@ Hy3NodeData::~Hy3NodeData() { } } -Hy3NodeData::Hy3NodeData(const Hy3NodeData& from): type(from.type) { - Debug::log(LOG, "Copy CTor type matches? %d is group? %d", this->type == from.type, this->type == Hy3NodeData::Group); - switch (from.type) { - case Hy3NodeData::Window: - this->as_window = from.as_window; - break; - case Hy3NodeData::Group: - new(&this->as_group) Hy3GroupData(from.as_group); - break; - } -} - Hy3NodeData::Hy3NodeData(Hy3NodeData&& from): type(from.type) { Debug::log(LOG, "Move CTor type matches? %d is group? %d", this->type == from.type, this->type == Hy3NodeData::Group); switch (from.type) { @@ -65,7 +53,7 @@ Hy3NodeData::Hy3NodeData(Hy3NodeData&& from): type(from.type) { } } -Hy3NodeData& Hy3NodeData::operator=(const Hy3NodeData& from) { +Hy3NodeData& Hy3NodeData::operator=(Hy3NodeData&& from) { Debug::log(LOG, "operator= type matches? %d is group? %d", this->type == from.type, this->type == Hy3NodeData::Group); if (this->type == Hy3NodeData::Group) { this->as_group.~Hy3GroupData(); @@ -78,7 +66,7 @@ Hy3NodeData& Hy3NodeData::operator=(const Hy3NodeData& from) { this->as_window = from.as_window; break; case Hy3NodeData::Group: - new(&this->as_group) Hy3GroupData(from.as_group); + new(&this->as_group) Hy3GroupData(std::move(from.as_group)); break; } @@ -121,36 +109,44 @@ void Hy3Node::recalcSizePosRecursive(bool force) { errorNotif(); } - double distortOut; - double distortIn; + double distort_out; + double distort_in; + double tab_height_offset; - const auto* gaps_in = &g_pConfigManager->getConfigValuePtr("general:gaps_in")->intValue; - const auto* gaps_out = &g_pConfigManager->getConfigValuePtr("general:gaps_out")->intValue; + static const auto* gaps_in = &HyprlandAPI::getConfigValue(PHANDLE, "general:gaps_in")->intValue; + static const auto* gaps_out = &HyprlandAPI::getConfigValue(PHANDLE, "general:gaps_out")->intValue; + static const auto* tab_bar_height = &HyprlandAPI::getConfigValue(PHANDLE, "plugin:hy3:tabs:bar_height")->intValue; + + tab_height_offset = *gaps_in * 2 + *tab_bar_height; if (gaps_in > gaps_out) { - distortOut = *gaps_out - 1.0; + distort_out = *gaps_out - 1.0; } else { - distortOut = *gaps_in - 1.0; + distort_out = *gaps_in - 1.0; } - if (distortOut < 0) distortOut = 0.0; + if (distort_out < 0) distort_out = 0.0; - distortIn = *gaps_in * 2; + distort_in = *gaps_in * 2; switch (group->layout) { case Hy3GroupLayout::SplitH: - child->position.x = this->position.x - distortOut; - child->size.x = this->size.x - distortIn; + child->position.x = this->position.x - distort_out; + child->size.x = this->size.x - distort_in; child->position.y = this->position.y; child->size.y = this->size.y; break; case Hy3GroupLayout::SplitV: - child->position.y = this->position.y - distortOut; - child->size.y = this->size.y - distortIn; + child->position.y = this->position.y - distort_out; + child->size.y = this->size.y - distort_in; child->position.x = this->position.x; child->size.x = this->size.x; + break; case Hy3GroupLayout::Tabbed: - // TODO + child->position.y = this->position.y + tab_height_offset; + child->size.y = this->size.y - tab_height_offset; + child->position.x = this->position.x; + child->size.x = this->size.x; break; } @@ -182,6 +178,7 @@ void Hy3Node::recalcSizePosRecursive(bool force) { offset += child->size.x; child->position.y = this->position.y; child->size.y = this->size.y; + //child->setHidden(false); break; case Hy3GroupLayout::SplitV: child->position.y = this->position.y + offset; @@ -189,11 +186,14 @@ void Hy3Node::recalcSizePosRecursive(bool force) { offset += child->size.y; child->position.x = this->position.x; child->size.x = this->size.x; + //child->setHidden(false); break; case Hy3GroupLayout::Tabbed: - // TODO: tab bars - child->position = this->position; - child->size = this->size; + child->position.y = this->position.y + 20; + child->size.y = this->size.y - 20; + child->position.x = this->position.x; + child->size.x = this->size.x; + //child->setHidden(group->lastFocusedChild != child); break; } @@ -201,6 +201,62 @@ void Hy3Node::recalcSizePosRecursive(bool force) { } } +void Hy3Node::setHidden(bool hidden) { + switch (this->data.type) { + case Hy3NodeData::Window: + this->data.as_window->setHidden(hidden); + break; + case Hy3NodeData::Group: + for (auto* child: this->data.as_group.children) { + child->setHidden(hidden); + } + } +} + +bool Hy3Node::isUrgent() { + switch (this->data.type) { + case Hy3NodeData::Window: + return this->data.as_window->m_bIsUrgent; + case Hy3NodeData::Group: + for (auto* child: this->data.as_group.children) { + if (child->isUrgent()) return true; + } + + return false; + } +} + +std::string Hy3Node::getTitle() { + switch (this->data.type) { + case Hy3NodeData::Window: + return this->data.as_window->m_szTitle; + case Hy3NodeData::Group: + std::string title; + + switch (this->data.as_group.layout) { + case Hy3GroupLayout::SplitH: + title = "[H] "; + break; + case Hy3GroupLayout::SplitV: + title = "[V] "; + break; + case Hy3GroupLayout::Tabbed: + title = "[T] "; + break; + } + + if (this->data.as_group.lastFocusedChild == nullptr) { + title += "Group"; + } else { + title += this->data.as_group.lastFocusedChild->getTitle(); + } + + return title; + } + + return ""; +} + void Hy3Node::markFocused() { Hy3Node* node = this; @@ -384,8 +440,8 @@ bool Hy3GroupData::hasChild(Hy3Node* node) { void Hy3Node::swapData(Hy3Node& a, Hy3Node& b) { Hy3NodeData aData = std::move(a.data); - a.data = b.data; - b.data = aData; + a.data = std::move(b.data); + b.data = std::move(aData); if (a.data.type == Hy3NodeData::Group) { for (auto child: a.data.as_group.children) { @@ -687,6 +743,10 @@ void Hy3Layout::onWindowFocusChange(CWindow* window) { auto* node = this->getNodeFromWindow(window); if (node == nullptr) return; + if (node->parent != nullptr && node->parent->data.as_group.layout == Hy3GroupLayout::Tabbed) { + node->parent->recalcSizePosRecursive(); + } + node->markFocused(); } @@ -1066,6 +1126,8 @@ void Hy3Layout::replaceWindowDataWith(CWindow* from, CWindow* to) { this->applyNodeDataToWindow(node); } +std::unique_ptr renderHookPtr = std::make_unique(Hy3Layout::renderHook); + void Hy3Layout::onEnable() { for (auto &window : g_pCompositor->m_vWindows) { if (window->isHidden() @@ -1077,10 +1139,12 @@ void Hy3Layout::onEnable() { this->onWindowCreatedTiling(window.get()); } + HyprlandAPI::registerCallbackStatic(PHANDLE, "render", renderHookPtr.get()); selection_hook::enable(); } void Hy3Layout::onDisable() { + HyprlandAPI::unregisterCallback(PHANDLE, renderHookPtr.get()); selection_hook::disable(); this->nodes.clear(); } @@ -1198,7 +1262,7 @@ Hy3Node* Hy3Layout::shiftOrGetFocus(Hy3Node& node, ShiftDirection direction, boo // if we haven't gone up any levels and the group is in the same direction // there's no reason to wrap the root group. - if (shiftMatchesLayout(group.layout, direction)) break; + if (group.layout != Hy3GroupLayout::Tabbed && shiftMatchesLayout(group.layout, direction)) break; if (group.layout != Hy3GroupLayout::Tabbed && group.children.size() == 2 @@ -1413,3 +1477,48 @@ std::string Hy3Node::debugNode() { return buf.str(); } + +// Recursively render tabs on all tab groups. +void renderTabsRecursive(Hy3Node& node); + +// Render tabs for the provided node, blindly assuming it is a tab group. +void renderTabs(Hy3Node& node); + +void Hy3Layout::renderHook(void*, std::any data) { + auto render_stage = std::any_cast(data); + if (render_stage == RENDER_POST_WINDOWS) { + auto* monitor = g_pHyprOpenGL->m_RenderData.pMonitor; + auto workspace = monitor->activeWorkspace; + auto* root = g_Hy3Layout->getWorkspaceRootGroup(workspace); + + if (root != nullptr) renderTabsRecursive(*root); + } +} + +void renderTabsRecursive(Hy3Node& node) { + if (node.data.type == Hy3NodeData::Group) { + for (auto* child: node.data.as_group.children) { + if (node.data.as_group.layout != Hy3GroupLayout::Tabbed + || node.data.as_group.lastFocusedChild == child) + { + renderTabsRecursive(*child); + } + } + + if (node.data.as_group.layout == Hy3GroupLayout::Tabbed) { + renderTabs(node); + } + } +} + +void renderTabs(Hy3Node& node) { + auto& group = node.data.as_group; + + if (!group.tab_bar) { + group.tab_bar = std::unique_ptr(new Hy3TabGroup(node)); + } else { + group.tab_bar->updateWithGroup(node); + } + + group.tab_bar->renderTabBar(); +} diff --git a/src/Hy3Layout.hpp b/src/Hy3Layout.hpp index 7b34ae6..8e044d2 100644 --- a/src/Hy3Layout.hpp +++ b/src/Hy3Layout.hpp @@ -1,5 +1,8 @@ #pragma once +struct Hy3Node; +#include "TabGroup.hpp" + #include #include @@ -23,6 +26,7 @@ struct Hy3GroupData { Hy3GroupLayout layout = Hy3GroupLayout::SplitH; std::list children; Hy3Node* lastFocusedChild = nullptr; + std::unique_ptr tab_bar; bool hasChild(Hy3Node* child); @@ -30,7 +34,6 @@ struct Hy3GroupData { private: Hy3GroupData(Hy3GroupData&&) = default; - Hy3GroupData(const Hy3GroupData&) = default; friend class Hy3NodeData; }; @@ -54,9 +57,8 @@ public: //private: - I give up, C++ wins Hy3NodeData(Hy3GroupData); - Hy3NodeData(const Hy3NodeData&); Hy3NodeData(Hy3NodeData&&); - Hy3NodeData& operator=(const Hy3NodeData&); + Hy3NodeData& operator=(Hy3NodeData&&); }; struct Hy3Node { @@ -76,6 +78,9 @@ struct Hy3Node { void raiseToTop(); Hy3Node* getFocusedNode(); void updateDecos(); + void setHidden(bool hidden); + bool isUrgent(); + std::string getTitle(); bool operator==(const Hy3Node&) const; @@ -126,6 +131,8 @@ public: Hy3Node* getWorkspaceRootGroup(const int&); Hy3Node* getWorkspaceFocusedNode(const int&); + static void renderHook(void*, std::any); + std::list nodes; private: struct { diff --git a/src/TabGroup.cpp b/src/TabGroup.cpp new file mode 100644 index 0000000..d40f63a --- /dev/null +++ b/src/TabGroup.cpp @@ -0,0 +1,198 @@ +#include "globals.hpp" +#include "TabGroup.hpp" +#include "Hy3Layout.hpp" + +#include +#include + +void Hy3TabBar::updateWithGroupEntries(Hy3Node& group_node) { + if (group_node.data.type != Hy3NodeData::Group) return; + auto& group = group_node.data.as_group; + + auto entries_iter = this->entries.begin(); + auto group_iter = group.children.begin(); + + auto* root_node = &group_node; + while (root_node->parent != nullptr) root_node = root_node->parent; + Hy3Node* focused_node = root_node->getFocusedNode(); + + while (entries_iter != this->entries.end()) { + if (group_iter == group.children.end()) { + needs_redraw = true; + + while (entries_iter != this->entries.end()) { + entries_iter = this->entries.erase(entries_iter); + } + + return; + }; + + auto& entry = *entries_iter; + auto& node = **group_iter; + + std::string title = node.getTitle(); + bool urgent = node.isUrgent(); + bool focused = focused_node == &group_node + || focused_node == &node + || (node.data.type == Hy3NodeData::Group && node.data.as_group.hasChild(focused_node)); + + if (entry.urgent != urgent + || entry.focused != focused + || entry.window_title != title) + this->needs_redraw = true; + + entry.window_title = std::move(title); + entry.urgent = urgent; + entry.focused = focused; + + entries_iter = std::next(entries_iter); + group_iter = std::next(group_iter); + } + + while (group_iter != group.children.end()) { + needs_redraw = true; + + auto& node = **group_iter; + + this->entries.push_back({ + .window_title = node.getTitle(), + .urgent = node.isUrgent(), + .focused = focused_node == &group_node + || focused_node == &node + || (node.data.type == Hy3NodeData::Group && node.data.as_group.hasChild(focused_node)), + }); + + group_iter = std::next(group_iter); + } +} + +void Hy3TabBar::setPos(Vector2D pos) { + if (pos == this->pos) return; + needs_redraw = true; + this->pos = pos; +} + +void Hy3TabBar::setSize(Vector2D size) { + this->size = size; +} + +void Hy3TabBar::prepareTexture() { + auto bar_width = this->size.x; + auto bar_height = this->size.y; + + if (needs_redraw || this->texture.m_iTexID == 0) { + static const auto* rounding_setting = &HyprlandAPI::getConfigValue(PHANDLE, "plugin:hy3:tabs:rounding")->intValue; + auto rounding = *rounding_setting; + + auto cairo_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, this->size.x, this->size.y); + auto cairo = cairo_create(cairo_surface); + + // clear pixmap + cairo_save(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + cairo_paint(cairo); + cairo_restore(cairo); + + auto swidth = (double) bar_width / (double) this->entries.size(); + size_t i = 0; + + for (auto& entry: this->entries) { + auto width = swidth; + auto x = i * width; + + if (entry.focused) { + cairo_set_source_rgba(cairo, 0.0, 1.0, 0.7, 0.5); + } else { + cairo_set_source_rgba(cairo, 0.2, 0.7, 1.0, 0.5); + } + + if (i == this->entries.size() - 1) { + cairo_move_to(cairo, x + width - rounding, rounding); + cairo_arc(cairo, x + width - rounding, rounding, rounding, -90.0 * (M_PI / 180.0), 0.0); + cairo_rectangle(cairo, x + width - rounding, rounding, rounding, bar_height - rounding * 2); + cairo_move_to(cairo, x + width - rounding, bar_height - rounding); + cairo_arc(cairo, x + width - rounding, bar_height - rounding, rounding, 0.0, 90.0 * (M_PI / 180.0)); + width -= rounding; + } + + if (i == 0) { + cairo_move_to(cairo, x + rounding, rounding); + cairo_arc(cairo, x + rounding, rounding, rounding, -180.0 * (M_PI / 180.0), -90.0 * (M_PI / 180.0)); + cairo_rectangle(cairo, x, rounding, rounding, bar_height - rounding * 2); + cairo_move_to(cairo, x + rounding, bar_height - rounding); + cairo_arc(cairo, x + rounding, bar_height - rounding, rounding, -270.0 * (M_PI / 180.0), -180.0 * (M_PI / 180.0)); + x += rounding; + width -= rounding; + } + + cairo_rectangle(cairo, x, 0, width, bar_height); + cairo_fill(cairo); + + i++; + } + + auto data = cairo_image_surface_get_data(cairo_surface); + this->texture.allocate(); + + glBindTexture(GL_TEXTURE_2D, this->texture.m_iTexID); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + + #ifdef GLES32 + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); + #endif + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bar_width, bar_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + + cairo_destroy(cairo); + cairo_surface_destroy(cairo_surface); + } else { + glBindTexture(GL_TEXTURE_2D, this->texture.m_iTexID); + } +} + +Hy3TabGroup::Hy3TabGroup(Hy3Node& node) { + this->pos.create(AVARTYPE_VECTOR, g_pConfigManager->getAnimationPropertyConfig("windowsIn"), nullptr, AVARDAMAGE_NONE); + this->size.create(AVARTYPE_VECTOR, g_pConfigManager->getAnimationPropertyConfig("windowsIn"), nullptr, AVARDAMAGE_NONE); + Debug::log(LOG, "registered anims"); + this->pos.registerVar(); + this->size.registerVar(); + + this->updateWithGroup(node); + this->pos.warp(false); + this->size.warp(false); +} + +void Hy3TabGroup::updateWithGroup(Hy3Node& node) { + static const auto* gaps_in = &HyprlandAPI::getConfigValue(PHANDLE, "general:gaps_in")->intValue; + static const auto* bar_height = &HyprlandAPI::getConfigValue(PHANDLE, "plugin:hy3:tabs:bar_height")->intValue; + + auto tpos = node.position + Vector2D(*gaps_in, *gaps_in); + auto tsize = Vector2D(node.size.x - *gaps_in * 2, *bar_height); + + if (this->pos.goalv() != tpos) this->pos = tpos; + if (this->size.goalv() != tsize) this->size = tsize; + + this->bar.updateWithGroupEntries(node); +} + +void Hy3TabGroup::renderTabBar() { + + auto* monitor = g_pHyprOpenGL->m_RenderData.pMonitor; + auto scale = monitor->scale; + + auto pos = this->pos.vec(); + auto size = this->size.vec(); + + auto scaled_pos = Vector2D(std::round(pos.x * scale), std::round(pos.y * scale)); + auto scaled_size = Vector2D(std::round(size.x * scale), std::round(size.y * scale)); + auto box = wlr_box { scaled_pos.x, scaled_pos.y, scaled_size.x, scaled_size.y }; + + this->bar.setPos(scaled_pos); + this->bar.setSize(scaled_size); + + this->bar.prepareTexture(); + g_pHyprOpenGL->renderTexture(this->bar.texture, &box, 1.0); + g_pHyprRenderer->damageBox(&box); +} diff --git a/src/TabGroup.hpp b/src/TabGroup.hpp new file mode 100644 index 0000000..08d0de6 --- /dev/null +++ b/src/TabGroup.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +class Hy3TabGroup; + +#include "Hy3Layout.hpp" +#include + +struct Hy3TabBarEntry { + std::string window_title; + bool urgent = false; + bool focused = false; +}; + +class Hy3TabBar { +public: + CTexture texture; + + void updateWithGroupEntries(Hy3Node&); + void setPos(Vector2D); + void setSize(Vector2D); + + // Redraw the texture if necessary, and bind it to GL_TEXTURE_2D + void prepareTexture(); +private: + bool needs_redraw = true; + + std::vector entries; + // scaled pos/size + Vector2D pos; + Vector2D size; +}; + +class Hy3TabGroup { +public: + Hy3TabBar bar; + CAnimatedVariable pos; + CAnimatedVariable size; + + // initialize a group with the given node. UB if node is not a group. + Hy3TabGroup(Hy3Node&); + + // update tab bar with node position and data. UB if node is not a group. + void updateWithGroup(Hy3Node&); + // render the scaled tab bar on the current monitor. + void renderTabBar(); + +private: + // moving a Hy3TabGroup will unregister any active animations + Hy3TabGroup(Hy3TabGroup&&) = delete; +}; diff --git a/src/main.cpp b/src/main.cpp index f299fe9..309303b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,6 +45,8 @@ void dispatch_makegroup(std::string arg) { g_Hy3Layout->makeGroupOnWorkspace(workspace, Hy3GroupLayout::SplitH); } else if (arg == "v") { g_Hy3Layout->makeGroupOnWorkspace(workspace, Hy3GroupLayout::SplitV); + } else if (arg == "tab") { + g_Hy3Layout->makeGroupOnWorkspace(workspace, Hy3GroupLayout::Tabbed); } else if (arg == "opposite") { g_Hy3Layout->makeOppositeGroupOnWorkspace(workspace); } @@ -104,6 +106,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { selection_hook::init(); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hy3:no_gaps_when_only", SConfigValue{.intValue = 0}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hy3:tabs:bar_height", SConfigValue{.intValue = 15}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hy3:tabs:rounding", SConfigValue{.intValue = 3}); g_Hy3Layout = std::make_unique(); HyprlandAPI::addLayout(PHANDLE, "hy3", g_Hy3Layout.get());