e385b2625912202861c24ff4e37c51c7a6e6dcac
- Author
- outfoxxed <outfoxxed@outfoxxed.me>
- Committer
- outfoxxed <outfoxxed@outfoxxed.me>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/CHANGELOG.md b/CHANGELOG.md
2index 63784ccb03e0c7c3b8f82f029a313e6f65a92715..2bdfe7ca43e79d45a8a2ee841c6e4c339a9342e3 100644
3--- a/CHANGELOG.md
4+++ b/CHANGELOG.md
5@@ -1,5 +1,10 @@
6 # Changelog
7
8+# Upcoming
9+
10+- Removed no_gaps_when_only in favor of workspace rules.
11+- Massive internal refactors, unknown bugs have been added and removed.
12+
13 # hl0.53.0.1 and before
14
15 - Fixed tab bar stencil and damage rendering
16diff --git a/README.md b/README.md
17index 36cbc0e93eae0d389ffbc6c405b0f6b6af72cdc3..d22016f103d5dfef0f3634ff8efd9fa7279c8e23 100644
18--- a/README.md
19+++ b/README.md
20@@ -225,12 +225,6 @@ configuration options, and some explanation as to what they do.
21 ```conf
22 plugin {
23 hy3 {
24- # disable gaps when only one window is onscreen
25- # 0 - always show gaps
26- # 1 - hide gaps with a single window onscreen
27- # 2 - 1 but also show the window border
28- no_gaps_when_only = <int> # default: 0
29-
30 # policy controlling what happens when a node is removed from a group,
31 # leaving only a group
32 # 0 = remove the nested group
33diff --git a/flake.lock b/flake.lock
34index 51641dc19da0bb015d75e312b21f85c8b7a4cbbb..51f7d10b927bb0cb1f6b63de995bff0aaa21c89a 100644
35--- a/flake.lock
36+++ b/flake.lock
37@@ -20,11 +20,11 @@
38 ]
39 },
40 "locked": {
41- "lastModified": 1767024902,
42- "narHash": "sha256-sMdk6QkMDhIOnvULXKUM8WW8iyi551SWw2i6KQHbrrU=",
43+ "lastModified": 1772460678,
44+ "narHash": "sha256-NYaWs8fYJ38IgFld0hGSdT2LEVhrgO8SiRReBjIH7YY=",
45 "owner": "hyprwm",
46 "repo": "aquamarine",
47- "rev": "b8a0c5ba5a9fbd2c660be7dd98bdde0ff3798556",
48+ "rev": "5d2cb726b16ee349df443f84b64cff53221b6983",
49 "type": "github"
50 },
51 "original": {
52@@ -87,11 +87,11 @@
53 ]
54 },
55 "locked": {
56- "lastModified": 1753964049,
57- "narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=",
58+ "lastModified": 1772461003,
59+ "narHash": "sha256-pVICsV7FtcEeVwg5y/LFh3XFUkVJninm/P1j/JHzEbM=",
60 "owner": "hyprwm",
61 "repo": "hyprcursor",
62- "rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5",
63+ "rev": "b62396457b9cfe2ebf24fe05404b09d2a40f8ed7",
64 "type": "github"
65 },
66 "original": {
67@@ -116,11 +116,11 @@
68 ]
69 },
70 "locked": {
71- "lastModified": 1766946335,
72- "narHash": "sha256-MRD+Jr2bY11MzNDfenENhiK6pvN+nHygxdHoHbZ1HtE=",
73+ "lastModified": 1772461523,
74+ "narHash": "sha256-mI6A51do+hEUzeJKk9YSWfVHdI/SEEIBi2tp5Whq5mI=",
75 "owner": "hyprwm",
76 "repo": "hyprgraphics",
77- "rev": "4af02a3925b454deb1c36603843da528b67ded6c",
78+ "rev": "7d63c04b4a2dd5e59ef943b4b143f46e713df804",
79 "type": "github"
80 },
81 "original": {
82@@ -146,17 +146,17 @@
83 "xdph": "xdph"
84 },
85 "locked": {
86- "lastModified": 1768908865,
87- "narHash": "sha256-KkzYP2+FVo3wIb2oom2BT7RRGmlvTiEaDJApCpTdcWw=",
88+ "lastModified": 1773149218,
89+ "narHash": "sha256-m5Ewi+rgn18qPP54Um4O2zwZ02oiomlZssHvXYEUv3U=",
90 "owner": "hyprwm",
91 "repo": "hyprland",
92- "rev": "8f547c6fa089f91e7577947c426f692397e9a5cb",
93+ "rev": "59f9f2688ac508a0584d1462151195a6c4992f99",
94 "type": "github"
95 },
96 "original": {
97 "owner": "hyprwm",
98 "repo": "hyprland",
99- "rev": "8f547c6fa089f91e7577947c426f692397e9a5cb",
100+ "rev": "59f9f2688ac508a0584d1462151195a6c4992f99",
101 "type": "github"
102 }
103 },
104@@ -193,11 +193,11 @@
105 ]
106 },
107 "locked": {
108- "lastModified": 1767023960,
109- "narHash": "sha256-R2HgtVS1G3KSIKAQ77aOZ+Q0HituOmPgXW9nBNkpp3Q=",
110+ "lastModified": 1772467975,
111+ "narHash": "sha256-kipyuDBxrZq+beYpZqWzGvFWm4QbayW9agAvi94vDXY=",
112 "owner": "hyprwm",
113 "repo": "hyprland-guiutils",
114- "rev": "c2e906261142f5dd1ee0bfc44abba23e2754c660",
115+ "rev": "5e1c6b9025aaf4d578f3eff7c0eb1f0c197a9507",
116 "type": "github"
117 },
118 "original": {
119@@ -218,11 +218,11 @@
120 ]
121 },
122 "locked": {
123- "lastModified": 1765214753,
124- "narHash": "sha256-P9zdGXOzToJJgu5sVjv7oeOGPIIwrd9hAUAP3PsmBBs=",
125+ "lastModified": 1772460177,
126+ "narHash": "sha256-/6G/MsPvtn7bc4Y32pserBT/Z4SUUdBd4XYJpOEKVR4=",
127 "owner": "hyprwm",
128 "repo": "hyprland-protocols",
129- "rev": "3f3860b869014c00e8b9e0528c7b4ddc335c21ab",
130+ "rev": "1cb6db5fd6bb8aee419f4457402fa18293ace917",
131 "type": "github"
132 },
133 "original": {
134@@ -247,11 +247,11 @@
135 ]
136 },
137diff --git a/flake.nix b/flake.nix
138index acc511613f1588f12206e0753414c11624c1b908..e0b434451283036c4513e4f603588cbdc08246a3 100644
139--- a/flake.nix
140+++ b/flake.nix
141@@ -1,6 +1,6 @@
142 {
143 inputs = {
144- hyprland.url = "github:hyprwm/hyprland/8f547c6fa089f91e7577947c426f692397e9a5cb";
145+ hyprland.url = "github:hyprwm/hyprland/59f9f2688ac508a0584d1462151195a6c4992f99";
146 };
147
148 outputs = {
149diff --git a/src/Hy3Layout.cpp b/src/Hy3Layout.cpp
150index 9aecf48087e97739346aa28c4917cce66801f40c..2b5ca893cb16533b8da16655c3a7a81d3848b84b 100644
151--- a/src/Hy3Layout.cpp
152+++ b/src/Hy3Layout.cpp
153@@ -1,4 +1,3 @@
154-#include <any>
155 #include <cstdint>
156 #include <regex>
157 #include <set>
158@@ -10,7 +9,6 @@
159 #include <hyprland/src/desktop/DesktopTypes.hpp>
160 #include <hyprland/src/desktop/Workspace.hpp>
161 #include <hyprland/src/desktop/rule/Engine.hpp>
162-#include <hyprland/src/managers/LayoutManager.hpp>
163 #include <hyprland/src/managers/PointerManager.hpp>
164 #include <hyprland/src/managers/SeatManager.hpp>
165 #include <hyprland/src/managers/input/InputManager.hpp>
166@@ -25,24 +23,28 @@
167 #include "Hy3Node.hpp"
168 #include "TabGroup.hpp"
169 #include "globals.hpp"
170-#include "src/SharedDefs.hpp"
171-#include "src/desktop/view/WLSurface.hpp"
172-#include "src/desktop/view/Window.hpp"
173-#include "src/desktop/rule/Rule.hpp"
174-#include "src/desktop/types/OverridableVar.hpp"
175-#include "src/devices/IPointer.hpp"
176
177
178 using namespace Desktop::View;
179
180-PHLWORKSPACE workspace_for_action(bool allow_fullscreen) {
181- if (g_pLayoutManager->getCurrentLayout() != g_Hy3Layout.get()) return nullptr;
182+static CollapsePolicy nodeCollapsePolicy() {
183+ static const auto node_collapse_policy =
184+ ConfigValue<Hyprlang::INT>("plugin:hy3:node_collapse_policy");
185+
186+ switch (*node_collapse_policy) {
187+ case 0: return CollapsePolicy::SingleNodeGroups;
188+ case 1: return CollapsePolicy::InvalidOnly;
189+ default: return CollapsePolicy::EmptySplits;
190+ }
191+}
192
193+PHLWORKSPACE workspace_for_action(bool allow_fullscreen) {
194 auto workspace = Desktop::focusState()->monitor()->m_activeSpecialWorkspace;
195 if (!valid(workspace)) workspace = Desktop::focusState()->monitor()->m_activeWorkspace;
196
197 if (!valid(workspace)) return nullptr;
198 if (!allow_fullscreen && workspace->m_hasFullscreenWindow) return nullptr;
199+ if (!hy3InstanceForWorkspace(workspace)) return nullptr;
200
201 return workspace;
202 }
203@@ -66,174 +68,197 @@ std::string operationWorkspaceForName(const std::string& workspace) {
204 return workspace;
205 }
206
207-SP<HOOK_CALLBACK_FN> renderHookPtr;
208-SP<HOOK_CALLBACK_FN> windowTitleHookPtr;
209-SP<HOOK_CALLBACK_FN> urgentHookPtr;
210-SP<HOOK_CALLBACK_FN> tickHookPtr;
211-SP<HOOK_CALLBACK_FN> mouseButtonPtr;
212+Hy3Node* findTabBarAt(Hy3Node& node, Vector2D pos, Hy3Node** focused_node);
213+
214+Hy3Layout::Hy3Layout() {
215+ g_hy3Instances.insert(this);
216+
217+ m_windowActiveListener = Event::bus()->m_events.window.active.listen(
218+ [this](PHLWINDOW window, Desktop::eFocusReason) {
219+ if (!window) return;
220+ auto* node = this->getNodeFromWindow(window.get());
221+ if (!node) return;
222+ this->onWindowFocusChange(window);
223+ }
224+ );
225+
226+ m_mouseButtonListener = Event::bus()->m_events.input.mouse.button.listen(
227+ [this](IPointer::SButtonEvent event, Event::SCallbackInfo& info) {
228+ if (event.state != 1 || event.button != 272) return;
229+
230+ auto ptr_surface_resource = g_pSeatManager->m_state.pointerFocus.lock();
231+ if (!ptr_surface_resource) return;
232+
233+ auto ptr_surface = CWLSurface::fromResource(ptr_surface_resource);
234+ if (!ptr_surface) return;
235+
236+ auto view = ptr_surface->view();
237+ auto* window = dynamic_cast<Desktop::View::CWindow*>(view.get());
238+ if (!window || window->m_isFloating || window->isFullscreen()) return;
239+
240+ auto* node = this->getNodeFromWindow(window);
241+ if (!node) return;
242+
243+ Hy3Node* focus = nullptr;
244+ auto mouse_pos = g_pInputManager->getMouseCoordsInternal();
245+ auto* tab_node = findTabBarAt(*this->root, mouse_pos, &focus);
246+ if (!tab_node) return;
247+
248+ while (focus->is_group() && !focus->as_group().group_focused
249+ && focus->as_group().focused_child != nullptr)
250+ focus = focus->as_group().focused_child;
251+
252+ focus->focus(false, Desktop::FOCUS_REASON_CLICK);
253diff --git a/src/Hy3Layout.hpp b/src/Hy3Layout.hpp
254index 049e19b84766402a8ffff2299fcbf195929bfaa0..d4bb5bfd9119518309f685d8f446d49a905b66af 100644
255--- a/src/Hy3Layout.hpp
256+++ b/src/Hy3Layout.hpp
257@@ -9,10 +9,15 @@ enum class GroupEphemeralityOption {
258 ForceEphemeral,
259 };
260
261-#include <list>
262 #include <set>
263
264-#include <hyprland/src/layout/IHyprLayout.hpp>
265+#include <hyprland/src/layout/algorithm/TiledAlgorithm.hpp>
266+#include <hyprland/src/layout/algorithm/Algorithm.hpp>
267+#include <hyprland/src/layout/space/Space.hpp>
268+#include <hyprland/src/layout/target/Target.hpp>
269+#include <hyprland/src/layout/LayoutManager.hpp>
270+#include <hyprland/src/helpers/signal/Signal.hpp>
271+#include <hyprland/src/event/EventBus.hpp>
272
273 enum class ShiftDirection {
274 Left,
275@@ -27,6 +32,16 @@ inline static constexpr char getShiftDirectionChar(ShiftDirection direction) {
276 : 'r';
277 }
278
279+inline static Math::eDirection shiftToMathDirection(ShiftDirection direction) {
280+ switch (direction) {
281+ case ShiftDirection::Left: return Math::DIRECTION_LEFT;
282+ case ShiftDirection::Right: return Math::DIRECTION_RIGHT;
283+ case ShiftDirection::Up: return Math::DIRECTION_UP;
284+ case ShiftDirection::Down: return Math::DIRECTION_DOWN;
285+ }
286+ return Math::DIRECTION_DEFAULT;
287+}
288+
289 enum class Axis { None, Horizontal, Vertical };
290
291 #include "Hy3Node.hpp"
292@@ -82,35 +97,29 @@ enum class ExpandFullscreenOption {
293
294 PHLWORKSPACE workspace_for_action(bool allow_fullscreen = false);
295
296-class Hy3Layout: public IHyprLayout {
297+class Hy3Layout: public Layout::ITiledAlgorithm {
298 public:
299- void onWindowCreatedTiling(PHLWINDOW, eDirection = DIRECTION_DEFAULT) override;
300- void onWindowRemovedTiling(PHLWINDOW) override;
301- void onWindowFocusChange(PHLWINDOW) override;
302- bool isWindowTiled(PHLWINDOW) override;
303- void recalculateMonitor(const MONITORID& monitor_id) override;
304- void recalculateWindow(PHLWINDOW) override;
305- void resizeActiveWindow(const Vector2D& delta, eRectCorner corner, PHLWINDOW pWindow = nullptr)
306- override;
307- void
308- fullscreenRequestForWindow(PHLWINDOW, eFullscreenMode current_mode, eFullscreenMode target_mode)
309- override;
310- std::any layoutMessage(SLayoutMessageHeader header, std::string content) override;
311- SWindowRenderLayoutHints requestRenderHints(PHLWINDOW) override;
312- void switchWindows(PHLWINDOW, PHLWINDOW) override;
313- void moveWindowTo(PHLWINDOW, const std::string& direction, bool silent) override;
314- void alterSplitRatio(PHLWINDOW, float, bool) override;
315- std::string getLayoutName() override;
316- PHLWINDOW getNextWindowCandidate(PHLWINDOW) override;
317- void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) override;
318- bool isWindowReachable(PHLWINDOW) override;
319- void bringWindowToTop(PHLWINDOW) override;
320- Vector2D predictSizeForNewWindowTiled() override { return Vector2D(); }
321-
322- void onEnable() override;
323- void onDisable() override;
324-
325- void insertNode(Hy3Node& node);
326+ Hy3Layout();
327+ ~Hy3Layout() override;
328+
329+ // ITiledAlgorithm / IModeAlgorithm overrides
330+ void newTarget(SP<Layout::ITarget> target) override;
331+ void movedTarget(SP<Layout::ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt) override;
332+ void removeTarget(SP<Layout::ITarget> target) override;
333+ void resizeTarget(const Vector2D& delta, SP<Layout::ITarget> target, Layout::eRectCorner corner = Layout::CORNER_NONE) override;
334+ void recalculate() override;
335+ void recalcGeometry(bool no_animation = false);
336+ void swapTargets(SP<Layout::ITarget> a, SP<Layout::ITarget> b) override;
337+ void moveTargetInDirection(SP<Layout::ITarget> t, Math::eDirection dir, bool silent) override;
338+ std::expected<void, std::string> layoutMsg(const std::string_view& sv) override;
339+ std::optional<Vector2D> predictSizeForNewTarget() override;
340+ SP<Layout::ITarget> getNextCandidate(SP<Layout::ITarget> old) override;
341+
342+ // Hy3-specific public methods
343+ void insertNode(UP<Hy3Node> node, std::optional<Vector2D> focalPoint = std::nullopt);
344+ void onWindowFocusChange(PHLWINDOW window);
345+ void updateGroupBorderColors();
346+
347 void makeGroupOnWorkspace(
348 const CWorkspace* workspace,
349 Hy3GroupLayout,
350@@ -123,8 +132,8 @@ public:
351 void toggleTabGroupOnWorkspace(const CWorkspace* workspace);
352 void changeGroupToOppositeOnWorkspace(const CWorkspace* workspace);
353 void changeGroupEphemeralityOnWorkspace(const CWorkspace* workspace, bool ephemeral);
354- void makeGroupOn(Hy3Node*, Hy3GroupLayout, GroupEphemeralityOption);
355- void makeOppositeGroupOn(Hy3Node*, GroupEphemeralityOption);
356+ void makeGroupOn(Hy3Node&, Hy3GroupLayout, GroupEphemeralityOption);
357diff --git a/src/Hy3Node.cpp b/src/Hy3Node.cpp
358index 3c777862ebe5eedd80fad2ed3a69de60379b23fd..23395fbb047762e9abbd563efc71862a1c11fe4d 100644
359--- a/src/Hy3Node.cpp
360+++ b/src/Hy3Node.cpp
361@@ -1,6 +1,6 @@
362+#include <cstdint>
363 #include <sstream>
364 #include <stdexcept>
365-#include <variant>
366
367 #include <bits/ranges_util.h>
368 #include <hyprland/src/Compositor.hpp>
369@@ -14,410 +14,358 @@
370 #include "Hy3Layout.hpp"
371 #include "Hy3Node.hpp"
372 #include "globals.hpp"
373-#include "src/managers/input/InputManager.hpp"
374
375-const float MIN_RATIO = 0.0f;
376+using Desktop::View::CWindow;
377
378-// Hy3GroupData //
379+const float MIN_RATIO = 0.0f;
380
381-Hy3GroupData::Hy3GroupData(Hy3GroupLayout layout): layout(layout) {
382- if (layout != Hy3GroupLayout::Tabbed) {
383+Hy3GroupNode::Hy3GroupNode(Hy3GroupLayout layout): layout(layout) {
384+ if (!isTab()) {
385 this->previous_nontab_layout = layout;
386 }
387 }
388
389-Hy3GroupData::Hy3GroupData(Hy3GroupData&& from) {
390- this->layout = from.layout;
391- this->locked = from.locked;
392- this->previous_nontab_layout = from.previous_nontab_layout;
393- this->children = std::move(from.children);
394- this->group_focused = from.group_focused;
395- this->expand_focused = from.expand_focused;
396- this->focused_child = from.focused_child;
397- from.focused_child = nullptr;
398- this->tab_bar = from.tab_bar;
399- from.tab_bar = nullptr;
400+bool Hy3Node::is_root() { return is_group() && as_group().layout == Hy3GroupLayout::Root; }
401+bool Hy3Node::is_root_group() { return !is_root() && parent->is_root(); }
402+
403+Hy3RootNode::Hy3RootNode(Hy3Layout* layout)
404+ : Hy3GroupNode(Hy3GroupLayout::Root), algo(layout) {}
405+
406+Hy3RootNode* Hy3Node::root() {
407+ auto* node = this;
408+ while (!node->is_root() && node->parent.get() != nullptr) {
409+ node = node->parent.get();
410+ }
411+ return dynamic_cast<Hy3RootNode*>(node);
412+}
413+
414+Hy3Layout* Hy3Node::layout() {
415+ auto* r = root();
416+ return r ? r->algo : nullptr;
417 }
418
419-Hy3GroupData::~Hy3GroupData() {
420- if (this->tab_bar != nullptr) this->tab_bar->bar.beginDestroy();
421+void Hy3Node::assertNotRoot() {
422+ if (this->is_root()) {
423+ hy3_log(ERR, "assertNotRoot failed: node {:x} is root", (uintptr_t) this);
424+ throw std::runtime_error("operation called on root node");
425+ }
426 }
427
428-bool Hy3GroupData::hasChild(Hy3Node* node) {
429- for (auto child: this->children) {
430- if (child == node) return true;
431+bool Hy3GroupNode::hasChild(Hy3Node& node) {
432+ for (auto& child: this->children) {
433+ if (child.get() == &node) return true;
434
435- if (child->data.is_group()) {
436- if (child->data.as_group().hasChild(node)) return true;
437+ if (child->is_group()) {
438+ if (child->as_group().hasChild(node)) return true;
439 }
440 }
441
442 return false;
443 }
444
445-void Hy3GroupData::collapseExpansions() {
446+auto Hy3GroupNode::findChild(Hy3Node& child) -> std::list<UP<Hy3Node>>::iterator {
447+ for (auto it = children.begin(); it != children.end(); ++it) {
448+ if (it->get() == &child) return it;
449+ }
450+ return children.end();
451+}
452+
453+void Hy3GroupNode::insertChild(std::list<UP<Hy3Node>>::iterator pos, UP<Hy3Node> child) {
454+ child->parent = this->self;
455+ if (focused_child == nullptr) focused_child = child.get();
456+ children.insert(pos, std::move(child));
457+ if (ephemeral == Ephemeral::Staged && children.size() >= 2)
458+ ephemeral = Ephemeral::Active;
459+}
460+
461diff --git a/src/Hy3Node.hpp b/src/Hy3Node.hpp
462index 876a235ebb95165cf5ba82269aca7b5771df8f5a..5a9bf518af5b3a56fb0632a88009b7a4526ef0c8 100644
463--- a/src/Hy3Node.hpp
464+++ b/src/Hy3Node.hpp
465@@ -1,25 +1,29 @@
466 #pragma once
467
468 struct Hy3Node;
469-struct Hy3GroupData;
470+struct Hy3TargetNode;
471+struct Hy3GroupNode;
472+struct Hy3RootNode;
473 enum class Hy3GroupLayout;
474
475-#include <variant>
476+#include <generator>
477
478 #include <hyprland/src/defines.hpp>
479 #include <hyprland/src/desktop/view/Window.hpp>
480+#include <hyprland/src/layout/target/Target.hpp>
481
482 #include "Hy3Layout.hpp"
483 #include "TabGroup.hpp"
484
485 enum class Hy3GroupLayout {
486+ Root,
487 SplitH,
488 SplitV,
489 Tabbed,
490 };
491
492 enum class Hy3NodeType {
493- Window,
494+ Target,
495 Group,
496 };
497
498@@ -29,88 +33,63 @@ enum class ExpandFocusType {
499 Stack,
500 };
501
502-struct Hy3GroupData {
503- Hy3GroupLayout layout = Hy3GroupLayout::SplitH;
504- Hy3GroupLayout previous_nontab_layout = Hy3GroupLayout::SplitH;
505- std::list<Hy3Node*> children;
506- bool group_focused = true;
507- Hy3Node* focused_child = nullptr;
508- ExpandFocusType expand_focused = ExpandFocusType::NotExpanded;
509- bool ephemeral = false;
510- bool locked = false;
511- bool containment = false;
512- Hy3TabGroup* tab_bar = nullptr;
513-
514- Hy3GroupData(Hy3GroupLayout layout);
515- ~Hy3GroupData();
516-
517- bool hasChild(Hy3Node* child);
518- void collapseExpansions();
519- void setLayout(Hy3GroupLayout layout);
520- void setEphemeral(GroupEphemeralityOption ephemeral);
521-
522- Hy3GroupData(Hy3GroupData&&);
523- Hy3GroupData(const Hy3GroupData&) = delete;
524+enum class Ephemeral {
525+ Off,
526+ Staged,
527+ Active,
528+};
529
530- friend class Hy3NodeData;
531+enum class CollapsePolicy {
532+ InvalidOnly,
533+ EmptySplits,
534+ SingleNodeGroups,
535 };
536
537-class Hy3NodeData {
538-public:
539- Hy3NodeData() = default;
540- Hy3NodeData(Hy3GroupData);
541- Hy3NodeData(PHLWINDOW window);
542- Hy3NodeData(Hy3GroupLayout layout);
543- Hy3NodeData(Hy3NodeData&&);
544- ~Hy3NodeData() = default;
545+struct Hy3Node {
546+ WP<Hy3Node> parent;
547+ WP<Hy3Node> self; // set from owning UP at creation time
548+ CBox logicalBox;
549+ CBox visualBox;
550+ float size_ratio = 1.0;
551+ bool hidden = false;
552
553- Hy3NodeData& operator=(PHLWINDOW);
554- Hy3NodeData& operator=(Hy3GroupLayout);
555- Hy3NodeData& operator=(Hy3NodeData&&);
556+ virtual ~Hy3Node() = default;
557+ Hy3Node(const Hy3Node&) = delete;
558+ Hy3Node& operator=(const Hy3Node&) = delete;
559
560- bool operator==(const Hy3NodeData&) const;
561+ template<typename T> bool is() const { return dynamic_cast<const T*>(this) != nullptr; }
562+ template<typename T> T& as() { return dynamic_cast<T&>(*this); }
563+ template<typename T> const T& as() const { return dynamic_cast<const T&>(*this); }
564
565diff --git a/src/TabGroup.cpp b/src/TabGroup.cpp
566index 9bf79212dba48a003ffcb3a2a3ff5fefcd30775d..e135c0f3c3d50a35f20734c940ecd20b9175d07e 100644
567--- a/src/TabGroup.cpp
568+++ b/src/TabGroup.cpp
569@@ -46,7 +46,7 @@ CHyprColor merge_colors(Args... colors) {
570 return CHyprColor(Hyprgraphics::CColor(oklab), alpha);
571 }
572
573-Hy3TabBarEntry::Hy3TabBarEntry(Hy3TabBar& tab_bar, Hy3Node& node): tab_bar(tab_bar), node(node) {
574+Hy3TabBarEntry::Hy3TabBarEntry(Hy3TabBar& tab_bar, Hy3Node& node): tab_bar(tab_bar), node(&node) {
575 g_pAnimationManager->createAnimation(
576 0.0F,
577 this->active,
578@@ -122,7 +122,7 @@ Hy3TabBarEntry::Hy3TabBarEntry(Hy3TabBar& tab_bar, Hy3Node& node): tab_bar(tab_b
579 *this->fade_opacity = 1.0;
580 }
581
582-bool Hy3TabBarEntry::operator==(const Hy3Node& node) const { return this->node == node; }
583+bool Hy3TabBarEntry::operator==(const Hy3Node& node) const { return this->node == &node; }
584
585 bool Hy3TabBarEntry::operator==(const Hy3TabBarEntry& entry) const {
586 return this->node == entry.node;
587@@ -458,88 +458,78 @@ void Hy3TabBar::tick() {
588 if (this->entries.empty()) this->destroy = true;
589 }
590
591-void Hy3TabBar::updateNodeList(std::list<Hy3Node*>& nodes) {
592- std::list<std::list<Hy3TabBarEntry>::iterator> removed_entries;
593+void Hy3TabBar::updateNodeList(std::list<UP<Hy3Node>>& nodes) {
594+ std::list<Hy3TabBarEntry> pool;
595+ pool.splice(pool.begin(), this->entries);
596
597- auto entry = this->entries.begin();
598- auto node = nodes.begin();
599-
600- // move any out of order entries to removed_entries
601- while (node != nodes.end()) {
602- while (true) {
603- if (entry == this->entries.end()) goto exitloop;
604- if (*entry == **node) break;
605- removed_entries.push_back(entry);
606- entry = std::next(entry);
607- }
608+ for (auto node = nodes.begin(); node != nodes.end(); ++node) {
609+ auto match = std::find_if(pool.begin(), pool.end(), [&node](auto& entry) {
610+ return **node == entry;
611+ });
612
613- node = std::next(node);
614- entry = std::next(entry);
615+ if (match != pool.end()) {
616+ match->node = node->get();
617+ this->entries.splice(this->entries.end(), pool, match);
618+ }
619 }
620
621-exitloop:
622+ // TODO: index match is wrong if a move results in the addition of a node and destruction of another in one op
623+ auto entry = this->entries.begin();
624+ int node_index = 0;
625+ for (auto node = nodes.begin(); node != nodes.end(); ++node, ++node_index) {
626+ if (entry != this->entries.end() && entry->node == node->get()) {
627+ ++entry;
628+ continue;
629+ }
630
631- // move any extra entries to removed_entries
632- while (entry != this->entries.end()) {
633- removed_entries.push_back(entry);
634- entry = std::next(entry);
635+ auto match = std::find_if(pool.begin(), pool.end(), [&node_index](auto& entry) {
636+ return entry.lastIndex == node_index;
637+ });
638+
639+ if (match != pool.end()) {
640+ match->node = node->get();
641+ this->entries.splice(entry, pool, match);
642+ }
643 }
644
645 entry = this->entries.begin();
646- node = nodes.begin();
647-
648- // add missing entries, taking first from removed_entries
649- while (node != nodes.end()) {
650- if (entry == this->entries.end() || *entry != **node) {
651- if (std::find(removed_entries.begin(), removed_entries.end(), entry) != removed_entries.end())
652- {
653- entry = std::next(entry);
654- continue;
655- }
656-
657- auto moved =
658- std::find_if(removed_entries.begin(), removed_entries.end(), [&node](auto entry) {
659- return **node == *entry;
660- });
661-
662- if (moved != removed_entries.end()) {
663- this->entries.splice(entry, this->entries, *moved);
664- entry = *moved;
665- removed_entries.erase(moved);
666- } else {
667- entry = this->entries.emplace(entry, *this, **node);
668- }
669diff --git a/src/TabGroup.hpp b/src/TabGroup.hpp
670index 216396a1dcb87e4890fd5f36809bbd1cb5330129..dfddfc1cc18cbf59b96e5fa1f73f22e47fdf8953 100644
671--- a/src/TabGroup.hpp
672+++ b/src/TabGroup.hpp
673@@ -12,6 +12,23 @@ class Hy3TabBar;
674 #include <hyprland/src/render/Renderer.hpp>
675 #include <hyprland/src/render/Texture.hpp>
676
677+struct Hy3TabGroupWrapper {
678+ UP<Hy3TabGroup> inner;
679+
680+ Hy3TabGroupWrapper();
681+ Hy3TabGroupWrapper(UP<Hy3TabGroup> tg);
682+ Hy3TabGroupWrapper(Hy3TabGroupWrapper&&);
683+ Hy3TabGroupWrapper& operator=(Hy3TabGroupWrapper&&);
684+ ~Hy3TabGroupWrapper();
685+
686+ void release();
687+ Hy3TabGroupWrapper& operator=(UP<Hy3TabGroup> tg);
688+
689+ Hy3TabGroup* operator->() { return inner.get(); }
690+ Hy3TabGroup* get() { return inner.get(); }
691+ operator bool() const { return inner.get() != nullptr; }
692+};
693+
694 #include "Hy3Node.hpp"
695
696 struct Hy3TabBarEntry {
697@@ -27,7 +44,8 @@ struct Hy3TabBarEntry {
698 PHLANIMVAR<float> vertical_pos; // 0.0-1.0, user specified direction
699 PHLANIMVAR<float> fade_opacity; // 0.0-1.0
700 Hy3TabBar& tab_bar;
701- Hy3Node& node; // only used for comparison. do not deref.
702+ Hy3Node* node; // only used for comparison. do not deref.
703+ int lastIndex = -1;
704
705 struct {
706 float scale = 0.0;
707@@ -88,14 +106,13 @@ public:
708 void damageBox(const Vector2D* position, const Vector2D* size);
709
710 void tick();
711- void updateNodeList(std::list<Hy3Node*>& nodes);
712+ void updateNodeList(std::list<UP<Hy3Node>>& nodes);
713 void updateAnimations(bool warp = false);
714 void setSize(Vector2D);
715
716 std::list<Hy3TabBarEntry> entries;
717
718 private:
719- Hy3Node* focused_node = nullptr;
720 Vector2D size;
721
722 // Tab bar entries take a reference to `this`.
723@@ -125,6 +142,10 @@ public:
724 Hy3TabBar bar;
725 PHLANIMVAR<Vector2D> pos;
726 PHLANIMVAR<Vector2D> size;
727+ WP<Hy3TabGroup> self;
728+
729+ // Factory: creates a tab group, sets self WP, registers in g_tabGroups.
730+ static UP<Hy3TabGroup> create(Hy3Node& node);
731
732 // initialize a group with the given node. UB if node is not a group.
733 Hy3TabGroup(Hy3Node&);
734diff --git a/src/dispatchers.cpp b/src/dispatchers.cpp
735index eaa3642d8806e9ab52dc7064305716fdffe4738b..43d694a6f18910095136fe81223cff3aafdbc0e6 100644
736--- a/src/dispatchers.cpp
737+++ b/src/dispatchers.cpp
738@@ -9,9 +9,16 @@
739 #include "globals.hpp"
740 #include "src/SharedDefs.hpp"
741
742+static Hy3Layout* hy3InstanceForAction(bool allow_fullscreen = false) {
743+ auto workspace = workspace_for_action(allow_fullscreen);
744+ if (!valid(workspace)) return nullptr;
745+ return hy3InstanceForWorkspace(workspace);
746+}
747+
748 static SDispatchResult dispatch_makegroup(std::string value) {
749- auto workspace = workspace_for_action();
750- if (!valid(workspace)) return SDispatchResult {};
751+ auto* hy3 = hy3InstanceForAction();
752+ if (!hy3) return SDispatchResult {};
753+ auto ws = hy3->workspace();
754
755 auto args = CVarList(value);
756
757@@ -26,48 +33,49 @@ static SDispatchResult dispatch_makegroup(std::string value) {
758 }
759
760 if (args[0] == "h") {
761- g_Hy3Layout->makeGroupOnWorkspace(workspace.get(), Hy3GroupLayout::SplitH, ephemeral, toggle);
762+ hy3->makeGroupOnWorkspace(ws.get(), Hy3GroupLayout::SplitH, ephemeral, toggle);
763 } else if (args[0] == "v") {
764- g_Hy3Layout->makeGroupOnWorkspace(workspace.get(), Hy3GroupLayout::SplitV, ephemeral, toggle);
765+ hy3->makeGroupOnWorkspace(ws.get(), Hy3GroupLayout::SplitV, ephemeral, toggle);
766 } else if (args[0] == "tab") {
767- g_Hy3Layout->makeGroupOnWorkspace(workspace.get(), Hy3GroupLayout::Tabbed, ephemeral, toggle);
768+ hy3->makeGroupOnWorkspace(ws.get(), Hy3GroupLayout::Tabbed, ephemeral, toggle);
769 } else if (args[0] == "opposite") {
770- g_Hy3Layout->makeOppositeGroupOnWorkspace(workspace.get(), ephemeral);
771+ hy3->makeOppositeGroupOnWorkspace(ws.get(), ephemeral);
772 }
773 return SDispatchResult {};
774 }
775
776 static SDispatchResult dispatch_changegroup(std::string value) {
777- auto workspace = workspace_for_action();
778- if (!valid(workspace)) return SDispatchResult {};
779+ auto* hy3 = hy3InstanceForAction();
780+ if (!hy3) return SDispatchResult {};
781+ auto ws = hy3->workspace();
782
783 auto args = CVarList(value);
784
785 if (args[0] == "h") {
786- g_Hy3Layout->changeGroupOnWorkspace(workspace.get(), Hy3GroupLayout::SplitH);
787+ hy3->changeGroupOnWorkspace(ws.get(), Hy3GroupLayout::SplitH);
788 } else if (args[0] == "v") {
789- g_Hy3Layout->changeGroupOnWorkspace(workspace.get(), Hy3GroupLayout::SplitV);
790+ hy3->changeGroupOnWorkspace(ws.get(), Hy3GroupLayout::SplitV);
791 } else if (args[0] == "tab") {
792- g_Hy3Layout->changeGroupOnWorkspace(workspace.get(), Hy3GroupLayout::Tabbed);
793+ hy3->changeGroupOnWorkspace(ws.get(), Hy3GroupLayout::Tabbed);
794 } else if (args[0] == "untab") {
795- g_Hy3Layout->untabGroupOnWorkspace(workspace.get());
796+ hy3->untabGroupOnWorkspace(ws.get());
797 } else if (args[0] == "toggletab") {
798- g_Hy3Layout->toggleTabGroupOnWorkspace(workspace.get());
799+ hy3->toggleTabGroupOnWorkspace(ws.get());
800 } else if (args[0] == "opposite") {
801- g_Hy3Layout->changeGroupToOppositeOnWorkspace(workspace.get());
802+ hy3->changeGroupToOppositeOnWorkspace(ws.get());
803 }
804 return SDispatchResult {};
805 }
806
807 static SDispatchResult dispatch_setephemeral(std::string value) {
808- auto workspace = workspace_for_action();
809- if (!valid(workspace)) return SDispatchResult {};
810+ auto* hy3 = hy3InstanceForAction();
811+ if (!hy3) return SDispatchResult {};
812
813 auto args = CVarList(value);
814
815 bool ephemeral = args[0] == "true";
816
817- g_Hy3Layout->changeGroupEphemeralityOnWorkspace(workspace.get(), ephemeral);
818+ hy3->changeGroupEphemeralityOnWorkspace(hy3->workspace().get(), ephemeral);
819 return SDispatchResult {};
820 }
821
822@@ -80,8 +88,8 @@ std::optional<ShiftDirection> parseShiftArg(std::string arg) {
823 }
824
825 static SDispatchResult dispatch_movewindow(std::string value) {
826- auto workspace = workspace_for_action();
827- if (!valid(workspace)) return SDispatchResult {};
828+ auto* hy3 = hy3InstanceForAction();
829+ if (!hy3) return SDispatchResult {};
830
831 auto args = CVarList(value);
832
833@@ -100,14 +108,15 @@ static SDispatchResult dispatch_movewindow(std::string value) {
834 i++;
835 }
836
837- g_Hy3Layout->shiftWindow(workspace.get(), shift.value(), once, visible);
838diff --git a/src/globals.hpp b/src/globals.hpp
839index a286a3d440aafebc0f90b2eb5a4c63ca62e7631e..54062a0d38fc3d2113e27fc19d72982d28955383 100644
840--- a/src/globals.hpp
841+++ b/src/globals.hpp
842@@ -1,16 +1,37 @@
843 #pragma once
844
845+#include <set>
846 #include <type_traits>
847+#include <vector>
848
849 #include <hyprland/src/desktop/Workspace.hpp>
850 #include <hyprland/src/plugins/PluginAPI.hpp>
851 #include <hyprlang.hpp>
852
853 #include "Hy3Layout.hpp"
854+#include "TabGroup.hpp"
855 #include "log.hpp"
856
857 inline HANDLE PHANDLE = nullptr;
858-inline std::unique_ptr<Hy3Layout> g_Hy3Layout;
859+
860+// Suppress algorithm callbacks (newTarget/movedTarget/removeTarget) during
861+// cross-workspace moves where we manually manage the tree.
862+inline bool g_suppressInsert = false;
863+
864+inline std::set<Hy3Layout*> g_hy3Instances;
865+
866+inline std::vector<WP<Hy3TabGroup>> g_tabGroups;
867+inline std::vector<UP<Hy3TabGroup>> g_destroyingTabGroups;
868+
869+inline CHyprSignalListener g_renderListener;
870+inline CHyprSignalListener g_tickListener;
871+inline CHyprSignalListener g_windowTitleListener;
872+inline CHyprSignalListener g_urgentListener;
873+
874+inline Hy3Layout* hy3InstanceForWorkspace(PHLWORKSPACE ws) {
875+ if (!ws || !ws->m_space || !ws->m_space->algorithm()) return nullptr;
876+ return dynamic_cast<Hy3Layout*>(ws->m_space->algorithm()->tiledAlgo().get());
877+}
878
879 inline void errorNotif() {
880 HyprlandAPI::addNotificationV2(
881diff --git a/src/main.cpp b/src/main.cpp
882index d935b2f416adce697bed7e80e01838943681f04a..50829b3408152724ee250d06a98f17f690a3dba5 100644
883--- a/src/main.cpp
884+++ b/src/main.cpp
885@@ -1,11 +1,14 @@
886 #include <hyprland/src/Compositor.hpp>
887 #include <hyprland/src/config/ConfigDataValues.hpp>
888 #include <hyprland/src/plugins/PluginAPI.hpp>
889+#include <hyprland/src/render/Renderer.hpp>
890+#include <hyprland/src/render/OpenGL.hpp>
891 #include <hyprland/src/version.h>
892 #include <hyprlang.hpp>
893
894 #include "dispatchers.hpp"
895 #include "globals.hpp"
896+#include "TabGroup.hpp"
897
898 APICALL EXPORT std::string PLUGIN_API_VERSION() { return HYPRLAND_API_VERSION; }
899
900@@ -82,8 +85,66 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
901
902 #undef CONF
903
904- g_Hy3Layout = std::make_unique<Hy3Layout>();
905- HyprlandAPI::addLayout(PHANDLE, "hy3", g_Hy3Layout.get());
906+ HyprlandAPI::addTiledAlgo(PHANDLE, "hy3", &typeid(Hy3Layout), []() -> UP<Layout::ITiledAlgorithm> {
907+ return makeUnique<Hy3Layout>();
908+ });
909+
910+ g_renderListener = Event::bus()->m_events.render.stage.listen([](eRenderStage stage) {
911+ static bool rendering_normally = false;
912+ static std::vector<Hy3TabGroup*> rendered_groups;
913+
914+ switch (stage) {
915+ case RENDER_PRE_WINDOWS:
916+ rendering_normally = true;
917+ rendered_groups.clear();
918+ break;
919+ case RENDER_POST_WINDOW:
920+ if (!rendering_normally) break;
921+
922+ for (auto& wp: g_tabGroups) {
923+ auto* entry = wp.get();
924+ if (!entry) continue;
925+ if (!entry->hidden
926+ && entry->target_window == g_pHyprOpenGL->m_renderData.currentWindow.lock()
927+ && std::find(rendered_groups.begin(), rendered_groups.end(), entry)
928+ == rendered_groups.end())
929+ {
930+ g_pHyprRenderer->m_renderPass.add(makeUnique<Hy3TabPassElement>(entry));
931+ rendered_groups.push_back(entry);
932+ }
933+ }
934+ break;
935+ case RENDER_POST_WINDOWS: rendering_normally = false; break;
936+ default: break;
937+ }
938+ });
939+
940+ g_tickListener = Event::bus()->m_events.tick.listen([]() {
941+ for (auto& wp: g_tabGroups) {
942+ if (auto* tg = wp.get()) tg->tick();
943+ }
944+ std::erase_if(g_destroyingTabGroups, [](auto& up) { return up->bar.destroy; });
945+ std::erase_if(g_tabGroups, [](auto& wp) { return !wp; });
946+ });
947+
948+ g_windowTitleListener = Event::bus()->m_events.window.title.listen([](PHLWINDOW window) {
949+ if (!window) return;
950+ auto* hy3 = hy3InstanceForWorkspace(window->m_workspace);
951+ if (!hy3) return;
952+ auto* node = hy3->getNodeFromWindow(window.get());
953+ if (!node) return;
954+ node->updateTabBarRecursive();
955+ });
956+
957+ g_urgentListener = Event::bus()->m_events.window.urgent.listen([](PHLWINDOW window) {
958+ if (!window) return;
959+ window->m_isUrgent = true;
960+ auto* hy3 = hy3InstanceForWorkspace(window->m_workspace);
961+ if (!hy3) return;
962+ auto* node = hy3->getNodeFromWindow(window.get());
963+ if (!node) return;
964+ node->updateTabBarRecursive();
965+ });
966
967 registerDispatchers();
968
969@@ -92,4 +153,12 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
970 return {"hy3", "i3 like layout for hyprland", "outfoxxed", "0.1"};
971 }
972
973-APICALL EXPORT void PLUGIN_EXIT() {}
974+APICALL EXPORT void PLUGIN_EXIT() {
975+ g_renderListener.reset();
976+ g_tickListener.reset();
977+ g_windowTitleListener.reset();
978+ g_urgentListener.reset();
979+
980+ g_tabGroups.clear();
981+ g_destroyingTabGroups.clear();
982+}