Hy3Layout.cpp
52395 bytes
1#include <cstdint>
2#include <regex>
3#include <optional>
4#include <set>
5
6#include <dlfcn.h>
7#include <hyprland/src/Compositor.hpp>
8#include <hyprland/src/output/Monitor.hpp>
9#include <hyprland/src/state/WorkspaceState.hpp>
10#include <hyprland/src/state/MonitorState.hpp>
11#include <hyprland/src/desktop/state/GlobalWindowController.hpp>
12#include <hyprland/src/desktop/state/FocusState.hpp>
13#include <hyprland/src/config/ConfigManager.hpp>
14#include <hyprland/src/desktop/DesktopTypes.hpp>
15#include <hyprland/src/workspace/HLWorkspace.hpp>
16#include <hyprland/src/desktop/view/window/WindowPresentation.hpp>
17#include <hyprland/src/desktop/rule/Engine.hpp>
18#include <hyprland/src/managers/SeatManager.hpp>
19#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
20#include <hyprland/src/managers/input/InputManager.hpp>
21#include <hyprland/src/pointer/PointerController.hpp>
22#include <hyprland/src/pointer/PointerManager.hpp>
23#include <hyprland/src/plugins/PluginAPI.hpp>
24#include <hyprland/src/plugins/PluginSystem.hpp>
25#include <hyprland/src/xwayland/XWayland.hpp>
26#include <hyprland/src/config/shared/workspace/WorkspaceRuleManager.hpp>
27#include <hyprutils/math/Vector2D.hpp>
28#include <hyprland/src/config/shared/ConfigErrors.hpp>
29#include <hyprland/src/config/shared/complex/ComplexDataTypes.hpp>
30#include <hyprland/src/desktop/state/WindowState.hpp>
31#include <ranges>
32
33#include "log.hpp"
34#include "Hy3Layout.hpp"
35#include "Hy3Node.hpp"
36#include "TabGroup.hpp"
37#include "globals.hpp"
38
39
40using namespace Desktop::View;
41
42static CollapsePolicy nodeCollapsePolicy() {
43 static const auto node_collapse_policy =
44 CConfigValue<Config::INTEGER>("plugin:hy3:node_collapse_policy");
45
46 switch (*node_collapse_policy) {
47 case 0: return CollapsePolicy::SingleNodeGroups;
48 case 1: return CollapsePolicy::InvalidOnly;
49 default: return CollapsePolicy::EmptySplits;
50 }
51}
52
53PHLWORKSPACE workspace_for_action(bool allow_fullscreen) {
54 auto workspace = Desktop::focusState()->monitor()->m_activeSpecialWorkspace;
55 if (!valid(workspace)) workspace = Desktop::focusState()->monitor()->m_activeWorkspace;
56
57 if (!valid(workspace)) return nullptr;
58 if (!allow_fullscreen && Fullscreen::controller()->hasFullscreen(workspace)) return nullptr;
59 if (!hy3InstanceForWorkspace(workspace)) return nullptr;
60
61 return workspace;
62}
63
64std::string operationWorkspaceForName(const std::string& workspace) {
65 typedef std::string (*PHYPRSPLIT_GET_WORKSPACE_FN)(const std::string& workspace);
66
67 static auto* hyprsplitTransformer = []() {
68 for (auto& p: g_pPluginSystem->getAllPlugins()) {
69 if (p->m_name == "hyprsplit") {
70 return reinterpret_cast<PHYPRSPLIT_GET_WORKSPACE_FN>(
71 dlsym(p->m_handle, "hyprsplitGetWorkspace")
72 );
73 }
74 }
75
76 return reinterpret_cast<PHYPRSPLIT_GET_WORKSPACE_FN>(0);
77 }();
78
79 if (hyprsplitTransformer != 0) return hyprsplitTransformer(workspace);
80 return workspace;
81}
82
83Hy3Node* findTabBarAt(Hy3Node& node, Vector2D pos, Hy3Node** focused_node);
84
85Hy3Layout::Hy3Layout() {
86 g_hy3Instances.insert(this);
87
88 m_windowActiveListener = Event::bus()->m_events.window.active.listen(
89 [this](PHLWINDOW window, Desktop::eFocusReason) {
90 if (!window) {
91 this->updateGroupBorderColors();
92 return;
93 }
94
95 auto* node = this->getNodeFromWindow(window.get());
96
97 if (!node) {
98 this->updateGroupBorderColors();
99 return;
100 }
101
102 this->onWindowFocusChange(window);
103 }
104 );
105
106 m_mouseButtonListener = Event::bus()->m_events.input.mouse.button.listen(
107 [this](IPointer::SButtonEvent event, Event::SCallbackInfo& info) {
108 if (event.state != 1 || event.button != 272) return;
109
110 auto ptr_surface_resource = g_pSeatManager->m_state.pointerFocus.lock();
111 if (!ptr_surface_resource) return;
112
113 auto ptr_surface = CWLSurface::fromResource(ptr_surface_resource);
114 if (!ptr_surface) return;
115
116 auto view = ptr_surface->view();
117 auto* window = dynamic_cast<Desktop::View::CWindow*>(view.get());
118 if (!window || window->isFloating() || Fullscreen::controller()->isFullscreen(window->m_self.lock())) return;
119
120 auto* node = this->getNodeFromWindow(window);
121 if (!node) return;
122
123 Hy3Node* focus = nullptr;
124 auto mouse_pos = g_pInputManager->getMouseCoordsInternal();
125 auto* tab_node = findTabBarAt(*this->root, mouse_pos, &focus);
126 if (!tab_node) return;
127
128 while (focus->is_group() && !focus->as_group().group_focused
129 && focus->as_group().focused_child != nullptr)
130 focus = focus->as_group().focused_child;
131
132 focus->focus(false, Desktop::FOCUS_REASON_CLICK);
133 g_pInputManager->simulateMouseMovement();
134 this->recalcGeometry();
135
136 info.cancelled = true;
137 }
138 );
139}
140
141Hy3Layout::~Hy3Layout() {
142 if (this->root) {
143 for (auto& window: this->root->windows()) {
144 window.setHidden(false);
145 }
146 }
147 this->root.reset();
148
149 g_hy3Instances.erase(this);
150}
151
152PHLWORKSPACE Hy3Layout::workspace() {
153 auto algo = m_parent.lock();
154 if (!algo) return nullptr;
155 auto space = algo->space();
156 if (!space) return nullptr;
157 return space->workspace();
158}
159
160PHLMONITORREF Hy3Layout::monitor() {
161 auto ws = workspace();
162 if (!ws) return nullptr;
163 return ws->m_monitor;
164}
165
166// ITiledAlgorithm overrides
167
168void Hy3Layout::newTarget(SP<Layout::ITarget> target) {
169 if (g_suppressInsert) return;
170 auto window = target->window();
171 if (!window) return;
172 hy3_log(
173 LOG,
174 "newTarget called with window {:x} (monitor: {}, workspace: {})",
175 (uintptr_t) window.get(),
176 window->monitorID(),
177 target->workspace() ? target->workspace()->addressableName() : "null"
178 );
179
180 auto* existing = this->getNodeFromTarget(target);
181 if (existing != nullptr) {
182 hy3_log(
183 ERR,
184 "newTarget called with a target ({:x}) that is already tiled (node: {:x})",
185 (uintptr_t) window.get(),
186 (uintptr_t) existing
187 );
188 return;
189 }
190
191 auto node = Hy3Node::create(target);
192
193 this->insertNode(std::move(node));
194}
195
196void Hy3Layout::insertNode(UP<Hy3Node> node_up, std::optional<Vector2D> focalPoint) {
197 if (node_up->parent != nullptr) {
198 hy3_log(
199 ERR,
200 "insertNode called for node {:x} which already has a parent ({:x})",
201 (uintptr_t) node_up.get(),
202 (uintptr_t) node_up->parent.get()
203 );
204 return;
205 }
206
207 auto ws = this->workspace();
208 if (!valid(ws)) {
209 hy3_log(
210 ERR,
211 "insertNode called for node {:x} with invalid workspace {}",
212 (uintptr_t) node_up.get(),
213 ws ? ws->addressableName() : "null"
214 );
215 return;
216 }
217
218 node_up->size_ratio = 1.0;
219
220 auto& monitor = ws->m_monitor;
221
222 Hy3Node* opening_into;
223 Hy3Node* opening_after = nullptr;
224
225 auto* rootNode = this->getWorkspaceRootGroup(ws.get());
226
227 if (rootNode != nullptr) {
228 if (focalPoint) {
229 auto window_at_point =
230 Desktop::viewState()->hitTest().windowAt(*focalPoint, RESERVED_EXTENTS | INPUT_EXTENTS);
231
232 if (window_at_point && window_at_point->m_workspace == ws) {
233 opening_after = this->getNodeFromWindow(window_at_point.get());
234 }
235 }
236
237 if (!opening_after) opening_after = &rootNode->getFocusedNode();
238 opening_after = &opening_after->getPlacementActor();
239
240 // opening_after->parent cannot be nullptr
241 if (opening_after == rootNode) {
242 opening_after->wrap(Hy3GroupLayout::SplitH, GroupEphemeralityOption::Standard);
243 }
244
245 opening_into = opening_after->parent.get();
246 } else {
247 static const auto tab_first_window =
248 CConfigValue<Config::INTEGER>("plugin:hy3:tab_first_window");
249
250 // Use space work area if available, fall back to monitor
251 CBox wa_box(monitor->m_position, monitor->m_size);
252 auto algo = m_parent.lock();
253 if (algo) {
254 auto space = algo->space();
255 if (space) wa_box = space->workArea();
256 }
257
258 auto rootUp = makeUnique<Hy3RootNode>(this);
259 rootUp->self = WP<Hy3Node>(rootUp);
260 this->root = std::move(rootUp);
261
262 UP<Hy3Node> rootGroup;
263 if (*tab_first_window) {
264 rootGroup = Hy3Node::create(Hy3GroupLayout::Tabbed);
265 } else {
266 auto split_layout =
267 wa_box.height > wa_box.width ? Hy3GroupLayout::SplitV : Hy3GroupLayout::SplitH;
268 rootGroup = Hy3Node::create(split_layout);
269 }
270
271 opening_into = rootGroup.get();
272 this->root->as_group().insertChild(std::move(rootGroup));
273 }
274
275 if (opening_into->is_target()) {
276 hy3_log(ERR, "opening_into node ({:x}) was not a group node", (uintptr_t) opening_into);
277 errorNotif();
278 return;
279 }
280
281 {
282 // clang-format off
283 static const auto at_enable = CConfigValue<Config::INTEGER>("plugin:hy3:autotile:enable");
284 static const auto at_ephemeral = CConfigValue<Config::INTEGER>("plugin:hy3:autotile:ephemeral_groups");
285 static const auto at_trigger_width = CConfigValue<Config::INTEGER>("plugin:hy3:autotile:trigger_width");
286 static const auto at_trigger_height = CConfigValue<Config::INTEGER>("plugin:hy3:autotile:trigger_height");
287 // clang-format on
288
289 this->updateAutotileWorkspaces();
290
291 auto& target_group = opening_into->as_group();
292 if (*at_enable && opening_after != nullptr && target_group.children.size() > 1
293 && target_group.isSplit()
294 && this->shouldAutotileWorkspace(ws.get()))
295 {
296 auto is_horizontal = target_group.layout == Hy3GroupLayout::SplitH;
297 auto trigger = is_horizontal ? *at_trigger_width : *at_trigger_height;
298 auto target_size = is_horizontal ? opening_into->visualBox.w : opening_into->visualBox.h;
299 auto size_after_addition = target_size / (target_group.children.size() + 1);
300
301 if (trigger >= 0 && (trigger == 0 || size_after_addition < trigger)) {
302 opening_after->wrap(
303 is_horizontal ? Hy3GroupLayout::SplitV : Hy3GroupLayout::SplitH,
304 *at_ephemeral ? GroupEphemeralityOption::Ephemeral : GroupEphemeralityOption::Standard
305 );
306 opening_into = opening_after->parent.get();
307 }
308 }
309 }
310
311 // For mouse drops, determine if we should insert before or after the target node
312 if (focalPoint && opening_after) {
313 auto& parentGroup = opening_into->as_group();
314 bool insert_before = false;
315
316 if (parentGroup.layout == Hy3GroupLayout::SplitH) {
317 insert_before = focalPoint->x < opening_after->visualBox.x + opening_after->visualBox.w * 0.5;
318 } else if (parentGroup.layout == Hy3GroupLayout::SplitV) {
319 insert_before = focalPoint->y < opening_after->visualBox.y + opening_after->visualBox.h * 0.5;
320 }
321
322 if (insert_before) {
323 auto iter = parentGroup.findChild(*opening_after);
324 if (iter != parentGroup.children.begin()) {
325 opening_after = std::prev(iter)->get();
326 } else {
327 opening_after = nullptr;
328 }
329 }
330 }
331
332 auto* node = node_up.get();
333
334 {
335 auto& group = opening_into->as_group();
336 if (opening_after == nullptr) {
337 group.insertChild(group.children.begin(), std::move(node_up));
338 } else {
339 auto iter = group.findChild(*opening_after);
340 group.insertChild(std::next(iter), std::move(node_up));
341 }
342 }
343
344 hy3_log(
345 LOG,
346 "tiled node {:x} inserted {} node {:x} in node {:x}",
347 (uintptr_t) node,
348 opening_after ? "after" : "at beginning of",
349 (uintptr_t) opening_after,
350 (uintptr_t) opening_into
351 );
352
353 node->markFocused();
354 this->recalcGeometry();
355 this->updateGroupBorderColors();
356}
357
358void Hy3Layout::movedTarget(SP<Layout::ITarget> target, std::optional<Vector2D> focalPoint) {
359 if (g_suppressInsert) return;
360
361 // Use mouse position as focal point when none provided (e.g. DnD drop)
362 if (!focalPoint) focalPoint = g_pInputManager->getMouseCoordsInternal();
363
364 this->insertNode(Hy3Node::create(target), focalPoint);
365}
366
367void Hy3Layout::removeTarget(SP<Layout::ITarget> target) {
368 if (g_suppressInsert) return;
369
370 auto* node = this->getNodeFromTarget(target);
371 if (node == nullptr) return;
372
373 auto window = node->as_window();
374
375 hy3_log(
376 LOG,
377 "removing target (window {:x} as node {:x}) from node {:x}",
378 (uintptr_t) window.get(),
379 (uintptr_t) node,
380 (uintptr_t) node->parent.get()
381 );
382
383 window->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT);
384
385 auto* parent_node = node->parent.get();
386 // Extracted UP drops โ node is destroyed.
387 parent_node->extractAndMerge(*node, nullptr, CollapsePolicy::InvalidOnly);
388 this->recalcGeometry();
389
390 this->updateGroupBorderColors();
391}
392
393void Hy3Layout::onWindowFocusChange(PHLWINDOW window) {
394 auto* node = this->getNodeFromWindow(window.get());
395 if (node == nullptr) return;
396
397 hy3_log(
398 TRACE,
399 "changing window focus to window {:x} as node {:x}",
400 (uintptr_t) window.get(),
401 (uintptr_t) node
402 );
403
404 node->markFocused();
405 this->recalcGeometry();
406
407 this->updateGroupBorderColors();
408}
409
410void Hy3Layout::updateGroupBorderColors() {
411 if (!this->root) return;
412 static auto active_color = CConfigValue<Config::IComplexConfigValue>("general:col.active_border");
413 auto* const active_color_data = sc<Config::CGradientValueData*>(active_color.ptr());
414
415 for (auto& w: this->root->windows()) {
416 if (this->shouldRenderSelected(&w)) {
417 w.m_ruleApplicator->inactiveBorderColor().set(*active_color_data, Desktop::Types::PRIORITY_LAYOUT);
418 } else {
419 w.m_ruleApplicator->inactiveBorderColor().unset(Desktop::Types::PRIORITY_LAYOUT);
420 }
421
422 w.presentation().refreshValues();
423 }
424}
425
426void Hy3Layout::recalculate(Layout::eRecalculateReason) { this->recalcGeometry(); }
427
428void Hy3Layout::recalcGeometry(bool no_animation) {
429 auto algo = m_parent.lock();
430 if (!algo) return;
431 auto space = algo->space();
432 if (!space) return;
433 auto workspace = space->workspace();
434 if (!workspace) return;
435
436 hy3_log(LOG, "recalculating workspace {}", workspace->addressableName());
437
438 auto ma = workspace->m_monitor->logicalBoxMinusReserved();
439 auto wa = space->workArea();
440
441 if (this->root) {
442 this->root->visualBox = wa;
443 this->root->recalcSizePosRecursive(CBox{
444 wa.x - ma.x,
445 wa.y - ma.y,
446 (ma.x + ma.w) - (wa.x + wa.w),
447 (ma.y + ma.h) - (wa.y + wa.h),
448 }, no_animation);
449 }
450}
451
452ShiftDirection reverse(ShiftDirection direction) {
453 switch (direction) {
454 case ShiftDirection::Left: return ShiftDirection::Right;
455 case ShiftDirection::Right: return ShiftDirection::Left;
456 case ShiftDirection::Up: return ShiftDirection::Down;
457 case ShiftDirection::Down: return ShiftDirection::Up;
458 default: return direction;
459 }
460}
461
462void Hy3Layout::resizeTarget(const Vector2D& delta, SP<Layout::ITarget> target, Layout::eRectCorner corner) {
463 auto* node = target ? this->getNodeFromTarget(target) : nullptr;
464 if (node == nullptr) return;
465
466 auto window = node->as_window();
467 if (!valid(window)) return;
468
469 node = &node->getExpandActor();
470
471 // Compare against work area since node position/size is the visible area
472 CBox workArea = {};
473 auto algo = this->m_parent.lock();
474 if (algo) {
475 auto space = algo->space();
476 if (space) workArea = space->workArea();
477 }
478
479 const bool display_left = STICKS(node->visualBox.x, workArea.x);
480 const bool display_right = STICKS(node->visualBox.x + node->visualBox.w, workArea.x + workArea.w);
481 const bool display_top = STICKS(node->visualBox.y, workArea.y);
482 const bool display_bottom = STICKS(node->visualBox.y + node->visualBox.h, workArea.y + workArea.h);
483
484 Vector2D resize_delta = delta;
485 bool node_is_root =
486 node->is_root()
487 || (node->is_target() && node->parent->is_root());
488
489 if (node_is_root) {
490 if (display_left && display_right) resize_delta.x = 0;
491 if (display_top && display_bottom) resize_delta.y = 0;
492 }
493
494 if (resize_delta.x == 0 && resize_delta.y == 0) return;
495
496 ShiftDirection target_edge_x;
497 ShiftDirection target_edge_y;
498
499 if (corner == Layout::CORNER_NONE) {
500 target_edge_x = display_right ? ShiftDirection::Left : ShiftDirection::Right;
501 target_edge_y = display_bottom ? ShiftDirection::Up : ShiftDirection::Down;
502
503 if (target_edge_x == ShiftDirection::Left) resize_delta.x = -resize_delta.x;
504 if (target_edge_y == ShiftDirection::Up) resize_delta.y = -resize_delta.y;
505 } else {
506 target_edge_x = corner & Layout::CORNER_LEFT ? ShiftDirection::Left : ShiftDirection::Right;
507 target_edge_y = corner & Layout::CORNER_TOP ? ShiftDirection::Up : ShiftDirection::Down;
508 }
509
510 auto horizontal_neighbor = node->findNeighbor(target_edge_x);
511 auto vertical_neighbor = node->findNeighbor(target_edge_y);
512
513 static const auto animate = CConfigValue<Config::INTEGER>("misc:animate_manual_resizes");
514
515 if (horizontal_neighbor) {
516 horizontal_neighbor->resize(reverse(target_edge_x), resize_delta.x, *animate == 0);
517 }
518
519 if (vertical_neighbor) {
520 vertical_neighbor->resize(reverse(target_edge_y), resize_delta.y, *animate == 0);
521 }
522}
523
524void Hy3Layout::swapTargets(SP<Layout::ITarget> a, SP<Layout::ITarget> b) {
525 // todo
526}
527
528void Hy3Layout::moveTargetInDirection(SP<Layout::ITarget> t, Math::eDirection dir, bool silent) {
529 auto* node = t ? this->getNodeFromTarget(t) : nullptr;
530 if (node == nullptr) return;
531
532 ShiftDirection shift;
533 switch (dir) {
534 case Math::DIRECTION_LEFT: shift = ShiftDirection::Left; break;
535 case Math::DIRECTION_RIGHT: shift = ShiftDirection::Right; break;
536 case Math::DIRECTION_UP: shift = ShiftDirection::Up; break;
537 case Math::DIRECTION_DOWN: shift = ShiftDirection::Down; break;
538 default: return;
539 }
540
541 this->shiftNode(*node, shift, false, false);
542}
543
544Config::ErrorResult Hy3Layout::layoutMsg(const std::string_view& sv) {
545 std::string content(sv);
546
547 if (content == "togglesplit") {
548 auto window = Desktop::focusState()->window();
549 if (!window) return {};
550 auto* node = this->getNodeFromWindow(window.get());
551 if (node != nullptr) {
552 node->assertNotRoot();
553 auto& layout = node->parent->as_group().layout;
554
555 switch (layout) {
556 case Hy3GroupLayout::SplitH:
557 layout = Hy3GroupLayout::SplitV;
558 this->recalcGeometry();
559 break;
560 case Hy3GroupLayout::SplitV:
561 layout = Hy3GroupLayout::SplitH;
562 this->recalcGeometry();
563 break;
564 case Hy3GroupLayout::Root: break;
565 case Hy3GroupLayout::Tabbed: break;
566 }
567 }
568 }
569
570 return {};
571}
572
573std::optional<Vector2D> Hy3Layout::predictSizeForNewTarget() {
574 return std::nullopt;
575}
576
577SP<Layout::ITarget> Hy3Layout::getNextCandidate(SP<Layout::ITarget> old) {
578 auto window = old ? old->window() : nullptr;
579 if (!window) return nullptr;
580
581 auto candidate = this->findTiledWindowCandidate(window.get());
582 if (!candidate) return nullptr;
583
584 auto* node = this->getNodeFromWindow(candidate.get());
585 if (!node) return nullptr;
586 return node->as_target();
587}
588
589PHLWINDOW Hy3Layout::findTiledWindowCandidate(const CWindow* from) {
590 auto* node = this->getWorkspaceFocusedNode(from->m_workspace.get(), true);
591 if (node != nullptr && node->is_target()) {
592 return node->as_window();
593 }
594
595 return PHLWINDOW();
596}
597
598PHLWINDOW Hy3Layout::findFloatingWindowCandidate(const CWindow* from) {
599 // return the first floating window on the same workspace that has not asked not to be focused
600 for (const auto& w: Desktop::windowState()->windows() | std::views::reverse) {
601 if (w->mapped() && !w->isHidden() && w->isFloating() && !w->backend().traits().overrideRedirect
602 && w->m_workspace == from->m_workspace && !w->shouldntFocus()
603 && !w->m_ruleApplicator->noFocus().valueOrDefault() && w.get() != from)
604 {
605 return w;
606 }
607 }
608
609 return nullptr;
610}
611
612void Hy3Layout::makeGroupOnWorkspace(
613 const Workspace::CHLWorkspace* workspace,
614 Hy3GroupLayout layout,
615 GroupEphemeralityOption ephemeral,
616 bool toggle
617) {
618 auto* node = this->getWorkspaceFocusedNode(workspace);
619 if (node == nullptr) return;
620 node = &node->getPlacementActor();
621
622 if (toggle) {
623 auto* parent = node->parent.get();
624 auto& group = parent->as_group();
625
626 if (group.children.size() == 1 && group.layout == layout) {
627 auto* collapsed = parent->collapseParents(CollapsePolicy::SingleNodeGroups);
628
629 if (collapsed && !collapsed->is_root()) {
630 collapsed->parent->updateTabBarRecursive();
631 this->recalcGeometry();
632 }
633
634 return;
635 }
636 }
637
638 this->makeGroupOn(*node, layout, ephemeral);
639}
640
641void Hy3Layout::makeOppositeGroupOnWorkspace(
642 const Workspace::CHLWorkspace* workspace,
643 GroupEphemeralityOption ephemeral
644) {
645 auto* node = this->getWorkspaceFocusedNode(workspace);
646 if (node == nullptr) return;
647 node = &node->getPlacementActor();
648 this->makeOppositeGroupOn(*node, ephemeral);
649}
650
651void Hy3Layout::changeGroupOnWorkspace(const Workspace::CHLWorkspace* workspace, Hy3GroupLayout layout) {
652 auto* node = this->getWorkspaceFocusedNode(workspace);
653 if (node == nullptr) return;
654 node = &node->getPlacementActor();
655
656 this->changeGroupOn(*node, layout);
657}
658
659void Hy3Layout::untabGroupOnWorkspace(const Workspace::CHLWorkspace* workspace) {
660 auto* node = this->getWorkspaceFocusedNode(workspace);
661 if (node == nullptr) return;
662 node = &node->getPlacementActor();
663
664 this->untabGroupOn(*node);
665}
666
667void Hy3Layout::toggleTabGroupOnWorkspace(const Workspace::CHLWorkspace* workspace) {
668 auto* node = this->getWorkspaceFocusedNode(workspace);
669 if (node == nullptr) return;
670 node = &node->getPlacementActor();
671
672 this->toggleTabGroupOn(*node);
673}
674
675void Hy3Layout::changeGroupToOppositeOnWorkspace(const Workspace::CHLWorkspace* workspace) {
676 auto* node = this->getWorkspaceFocusedNode(workspace);
677 if (node == nullptr) return;
678 node = &node->getPlacementActor();
679
680 this->changeGroupToOppositeOn(*node);
681}
682
683void Hy3Layout::changeGroupEphemeralityOnWorkspace(const Workspace::CHLWorkspace* workspace, bool ephemeral) {
684 auto* node = this->getWorkspaceFocusedNode(workspace);
685 if (node == nullptr) return;
686 node = &node->getPlacementActor();
687
688 this->changeGroupEphemeralityOn(*node, ephemeral);
689}
690
691void Hy3Layout::makeGroupOn(
692 Hy3Node& node,
693 Hy3GroupLayout layout,
694 GroupEphemeralityOption ephemeral
695) {
696 node.assertNotRoot();
697
698 hy3_log(LOG, "mkGrp on {:x} b4\n{}", (uintptr_t)&node, debugNodes());
699
700 node.wrap(layout, ephemeral);
701 node.parent->collapseParents(CollapsePolicy::InvalidOnly);
702 this->recalcGeometry();
703}
704
705void Hy3Layout::makeOppositeGroupOn(Hy3Node& node, GroupEphemeralityOption ephemeral) {
706 node.assertNotRoot();
707
708 auto& group = node.parent->as_group();
709 auto layout =
710 group.layout == Hy3GroupLayout::SplitH ? Hy3GroupLayout::SplitV : Hy3GroupLayout::SplitH;
711
712 if (group.children.size() == 1) {
713 group.setLayout(layout);
714 group.setEphemeral(ephemeral);
715 this->recalcGeometry();
716 return;
717 }
718
719 node.wrap(layout, ephemeral);
720}
721
722void Hy3Layout::changeGroupOn(Hy3Node& node, Hy3GroupLayout layout) {
723 node.assertNotRoot();
724 auto& group = node.parent->as_group();
725 group.setLayout(layout);
726 node.parent->updateTabBarRecursive();
727 this->recalcGeometry();
728}
729
730void Hy3Layout::untabGroupOn(Hy3Node& node) {
731 node.assertNotRoot();
732 auto& group = node.parent->as_group();
733 if (!group.isTab()) return;
734
735 changeGroupOn(node, group.previous_nontab_layout);
736}
737
738void Hy3Layout::toggleTabGroupOn(Hy3Node& node) {
739 node.assertNotRoot();
740 auto& group = node.parent->as_group();
741 if (!group.isTab()) changeGroupOn(node, Hy3GroupLayout::Tabbed);
742 else changeGroupOn(node, group.previous_nontab_layout);
743}
744
745void Hy3Layout::changeGroupToOppositeOn(Hy3Node& node) {
746 node.assertNotRoot();
747 auto& group = node.parent->as_group();
748
749 if (group.isTab()) {
750 group.setLayout(group.previous_nontab_layout);
751 } else {
752 group.setLayout(
753 group.layout == Hy3GroupLayout::SplitH ? Hy3GroupLayout::SplitV : Hy3GroupLayout::SplitH
754 );
755 }
756
757 this->recalcGeometry();
758}
759
760void Hy3Layout::changeGroupEphemeralityOn(Hy3Node& node, bool ephemeral) {
761 node.assertNotRoot();
762 auto& group = node.parent->as_group();
763 group.setEphemeral(
764 ephemeral ? GroupEphemeralityOption::ForceEphemeral : GroupEphemeralityOption::Standard
765 );
766}
767
768void Hy3Layout::shiftNode(Hy3Node& node, ShiftDirection direction, bool once, bool visible) {
769 this->shiftOrGetFocus(node, direction, true, once, visible);
770}
771
772void Hy3Layout::shiftWindow(
773 const Workspace::CHLWorkspace* workspace,
774 ShiftDirection direction,
775 bool once,
776 bool visible
777) {
778 auto* node = this->getWorkspaceFocusedNode(workspace);
779 if (node == nullptr) return;
780
781 this->shiftNode(*node, direction, once, visible);
782}
783
784void Hy3Layout::shiftFocus(
785 const Workspace::CHLWorkspace* workspace,
786 ShiftDirection direction,
787 bool visible,
788 bool warp
789) {
790 auto current_window = Desktop::focusState()->window();
791
792 if (current_window != nullptr) {
793 if (Fullscreen::controller()->hasFullscreen(current_window->m_workspace)) {
794 return;
795 }
796
797 if (current_window->isFloating()) {
798 auto next_window = Desktop::windowState()->query().inDirection(
799 current_window,
800 shiftToMathDirection(direction)
801 );
802
803 if (next_window != nullptr) {
804 g_pInputManager->unconstrainMouse();
805 Desktop::focusState()->fullWindowFocus(next_window, Desktop::FOCUS_REASON_KEYBIND);
806 if (warp) {
807 const auto box = next_window->layoutBox();
808 Hy3Layout::warpCursorToBox(box.pos(), box.size());
809 }
810 }
811 return;
812 }
813 }
814
815 auto* node = this->getWorkspaceFocusedNode(workspace);
816 if (node == nullptr) {
817 focusMonitor(direction);
818 return;
819 }
820
821 auto* target = this->shiftOrGetFocus(*node, direction, false, false, visible);
822
823 if (target != nullptr) {
824 if (warp) {
825 // don't warp for nodes in the same tab
826 warp = node->parent != target->parent
827 || !node->parent->as_group().isTab();
828 }
829
830 target->focus(warp, Desktop::FOCUS_REASON_KEYBIND);
831 this->recalcGeometry();
832 }
833}
834
835Hy3Node* Hy3Layout::focusMonitor(ShiftDirection direction) {
836 auto next_monitor = State::monitorState()
837 ->query()
838 .relativeTo(this->monitor().lock())
839 .inDirection(shiftToMathDirection(direction))
840 .run();
841
842 if (next_monitor) {
843 bool found = false;
844 Desktop::focusState()->rawMonitorFocus(next_monitor);
845 auto next_workspace = next_monitor->m_activeWorkspace;
846
847 if (next_workspace) {
848 auto target_window = next_workspace->getLastFocusedWindow();
849 if (target_window) {
850 found = true;
851
852 if (auto* hy3 = hy3InstanceForWorkspace(next_workspace)) {
853 auto found_node = hy3->getNodeFromWindow(target_window.get());
854 if (found_node) {
855 found_node->focus(true, Desktop::FOCUS_REASON_KEYBIND);
856 return found_node;
857 }
858 } else {
859 Desktop::focusState()->fullWindowFocus(target_window, Desktop::FOCUS_REASON_KEYBIND);
860 return nullptr;
861 }
862 }
863 }
864
865 if (!found) {
866 Hy3Layout::warpCursorWithFocus(next_monitor->m_position + next_monitor->m_size / 2);
867 }
868 }
869 return nullptr;
870}
871
872bool Hy3Layout::shiftMonitor(Hy3Node& node, ShiftDirection direction, bool follow) {
873 auto next_monitor = State::monitorState()
874 ->query()
875 .relativeTo(this->monitor().lock())
876 .inDirection(shiftToMathDirection(direction))
877 .run();
878
879
880 if (next_monitor) {
881 Desktop::focusState()->rawMonitorFocus(next_monitor);
882 auto next_workspace = next_monitor->m_activeWorkspace;
883 if (next_workspace) {
884 moveNodeToWorkspace(node.layout()->workspace().get(), next_workspace->addressableName(), follow, false);
885 return true;
886 }
887 }
888 return false;
889}
890
891void Hy3Layout::toggleFocusLayer(const Workspace::CHLWorkspace* workspace, bool warp) {
892 auto current_window = Desktop::focusState()->window();
893 if (!current_window) return;
894
895 PHLWINDOW target;
896 if (current_window->isFloating()) {
897 target = this->findTiledWindowCandidate(current_window.get());
898 } else {
899 target = this->findFloatingWindowCandidate(current_window.get());
900 }
901
902 if (!target) return;
903
904 Desktop::focusState()->fullWindowFocus(target, Desktop::FOCUS_REASON_KEYBIND);
905
906 if (warp) {
907 Hy3Layout::warpCursorWithFocus(target->middle());
908 }
909}
910
911void Hy3Layout::warpCursor() {
912 auto current_window = Desktop::focusState()->window();
913
914 if (current_window != nullptr) {
915 if (current_window != nullptr) {
916 Hy3Layout::warpCursorWithFocus(current_window->middle(), true);
917 }
918 } else {
919 auto* node =
920 this->getWorkspaceFocusedNode(Desktop::focusState()->monitor()->m_activeWorkspace.get());
921
922 if (node != nullptr) {
923 Hy3Layout::warpCursorWithFocus(node->visualBox.pos() + node->visualBox.size() / 2);
924 }
925 }
926}
927
928static void updateTreeTabBars(Hy3Node& node) {
929 node.updateTabBar();
930 if (node.is_group()) {
931 for (auto& child: node.as_group().children) {
932 updateTreeTabBars(*child);
933 }
934 }
935}
936
937
938void Hy3Layout::moveNodeToWorkspace(
939 Workspace::CHLWorkspace* origin,
940 std::string wsname,
941 bool follow,
942 bool warp
943) {
944 const auto targetName = operationWorkspaceForName(wsname);
945 const bool numbered = std::regex_match(targetName, std::regex(R"(^\d+$)"));
946
947 if (!numbered && !targetName.starts_with("special") && !targetName.starts_with("name:")) {
948 hy3_log(ERR, "moveNodeToWorkspace called with invalid workspace {}", wsname);
949 return;
950 }
951
952 auto workspace = State::workspaceState()->query().input(targetName).run();
953
954 if (origin == workspace.get()) return;
955
956 auto* node = this->getWorkspaceFocusedNode(origin);
957 auto focused_window = Desktop::focusState()->window();
958 auto* focused_window_node = this->getNodeFromWindow(focused_window.get());
959
960 auto origin_ws = node != nullptr ? node->layout()->workspace()
961 : focused_window != nullptr ? focused_window->m_workspace
962 : nullptr;
963
964 if (!valid(origin_ws)) return;
965
966 if (workspace == nullptr) {
967 hy3_log(LOG, "creating target workspace {} for node move", targetName);
968
969 auto origin_monitor = origin_ws->m_monitor.lock();
970 if (!origin_monitor) return;
971
972 if (numbered) {
973 workspace = State::workspaceState()->createNumbered(
974 Workspace::SWorkspaceNumberedID{static_cast<Workspace::WorkspaceIDContainer>(std::stol(targetName))},
975 origin_monitor
976 );
977 } else if (targetName == "special") {
978 workspace = State::workspaceState()->createSpecial("special", origin_monitor);
979 } else if (targetName.starts_with("special:") && targetName.size() > 8) {
980 workspace = State::workspaceState()->createSpecial(targetName.substr(8), origin_monitor);
981 } else {
982 const auto address = targetName.starts_with("name:") ? targetName.substr(5) : targetName;
983 workspace = State::workspaceState()->createNamed(address, origin_monitor, address);
984 }
985 }
986
987 if (focused_window != nullptr
988 && (focused_window_node == nullptr || Fullscreen::controller()->isFullscreen(focused_window)))
989 {
990 g_pHyprRenderer->damageWindow(focused_window);
991 Desktop::globalWindowController()->moveWindowToWorkspace(focused_window, workspace);
992 } else {
993 if (node == nullptr) return;
994
995 hy3_log(
996 LOG,
997 "moving node {:x} from workspace {} to workspace {} (follow: {})",
998 (uintptr_t) node,
999 origin->addressableName(),
1000 workspace->addressableName(),
1001 follow
1002 );
1003
1004 auto* parent_node = node->parent.get();
1005 auto node_up = parent_node->extractAndMerge(*node, nullptr);
1006 auto* destHy3 = hy3InstanceForWorkspace(workspace);
1007 auto* destLayout = destHy3 ? destHy3 : this;
1008
1009 g_suppressInsert = true;
1010
1011 for (auto& window: node->windows()) {
1012 window.layoutTarget()->assignToSpace(workspace->space());
1013 }
1014
1015 g_suppressInsert = false;
1016
1017 destLayout->insertNode(std::move(node_up));
1018
1019 Desktop::Rule::ruleEngine()->updateAllRules();
1020
1021 updateTreeTabBars(*node);
1022 node->updateTabBarRecursive();
1023 this->recalcGeometry();
1024 }
1025
1026 if (follow) {
1027 auto& monitor = workspace->m_monitor;
1028
1029 if (workspace->type() == Workspace::eWorkspaceType::SPECIAL) {
1030 monitor->setSpecialWorkspace(workspace);
1031 } else if (origin_ws->type() == Workspace::eWorkspaceType::SPECIAL) {
1032 origin_ws->m_monitor->setSpecialWorkspace(nullptr);
1033 }
1034
1035 monitor->changeWorkspace(workspace);
1036
1037 node->layout()->recalcGeometry();
1038 node->focus(warp, Desktop::FOCUS_REASON_KEYBIND);
1039 }
1040}
1041
1042void Hy3Layout::changeFocus(const Workspace::CHLWorkspace* workspace, FocusShift shift) {
1043 auto* node = this->getWorkspaceFocusedNode(workspace);
1044 if (node == nullptr) return;
1045
1046 switch (shift) {
1047 case FocusShift::Bottom: goto bottom;
1048 case FocusShift::Top:
1049 this->root->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1050 this->updateGroupBorderColors();
1051 return;
1052 case FocusShift::Raise:
1053 if (node->is_root_group()) goto bottom;
1054 node->parent->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1055 this->updateGroupBorderColors();
1056 return;
1057 case FocusShift::Lower:
1058 if (node->is_group() && node->as_group().focused_child != nullptr)
1059 node->as_group().focused_child->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1060 this->updateGroupBorderColors();
1061 return;
1062 case FocusShift::Tab:
1063 for (auto& n: node->ancestors()) {
1064 if (n.parent->as_group().isTab()) {
1065 n.parent->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1066 this->updateGroupBorderColors();
1067 return;
1068 }
1069 }
1070 return;
1071 case FocusShift::TabNode:
1072 for (auto& n: node->ancestors()) {
1073 if (n.parent->as_group().isTab()) {
1074 n.focus(false, Desktop::FOCUS_REASON_KEYBIND);
1075 this->updateGroupBorderColors();
1076 return;
1077 }
1078 }
1079 return;
1080 }
1081
1082bottom:
1083 while (node->is_group() && node->as_group().focused_child != nullptr) {
1084 node = node->as_group().focused_child;
1085 }
1086
1087 node->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1088 this->updateGroupBorderColors();
1089 return;
1090}
1091
1092Hy3Node* findTabBarAt(Hy3Node& node, Vector2D pos, Hy3Node** focused_node) {
1093 // clang-format off
1094 static const auto p_gaps_in = CConfigValue<Config::IComplexConfigValue>("general:gaps_in");
1095 static const auto tab_bar_height = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:height");
1096 static const auto tab_bar_padding = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding");
1097 // clang-format on
1098
1099 auto workspace_rule = Config::workspaceRuleMgr()->getWorkspaceRuleFor(node.layout()->workspace());
1100 auto gaps_in = workspace_rule.and_then([](auto r) { return r.m_gapsIn; }).value_or(*sc<Config::CCssGapData*>(p_gaps_in.ptr()));
1101
1102 auto inset = *tab_bar_height + *tab_bar_padding + gaps_in.m_top;
1103
1104 if (node.is_group()) {
1105 if (node.hidden) return nullptr;
1106 // note: tab bar clicks ignore animations
1107 if (node.visualBox.x > pos.x || node.logicalBox.y > pos.y || node.visualBox.x + node.visualBox.w < pos.x
1108 || node.visualBox.y + node.visualBox.h < pos.y)
1109 return nullptr;
1110
1111 auto& group = node.as_group();
1112
1113 if (group.isTab() && group.tab_bar) {
1114 if (pos.y < node.visualBox.y + inset) {
1115 auto& children = group.children;
1116 auto& tab_bar = *group.tab_bar.get();
1117
1118 auto size = tab_bar.size->value();
1119 auto x = pos.x - tab_bar.pos->value().x;
1120 auto child_iter = children.begin();
1121
1122 for (auto& tab: tab_bar.bar.entries) {
1123 if (child_iter == children.end()) break;
1124
1125 if (x > tab.offset->value() * size.x
1126 && x < (tab.offset->value() + tab.width->value()) * size.x)
1127 {
1128 *focused_node = child_iter->get();
1129 return &node;
1130 }
1131
1132 child_iter = std::next(child_iter);
1133 }
1134 }
1135
1136 if (group.focused_child != nullptr) {
1137 return findTabBarAt(*group.focused_child, pos, focused_node);
1138 }
1139 } else {
1140 for (auto& child: group.children) {
1141 if (findTabBarAt(*child, pos, focused_node)) return child.get();
1142 }
1143 }
1144 }
1145
1146 return nullptr;
1147}
1148
1149void Hy3Layout::focusTab(
1150 const Workspace::CHLWorkspace* workspace,
1151 TabFocus target,
1152 TabFocusMousePriority mouse,
1153 bool wrap_scroll,
1154 int index
1155) {
1156 auto* node = this->getWorkspaceRootGroup(workspace);
1157 if (node == nullptr) return;
1158
1159 Hy3Node* tab_node = nullptr;
1160 Hy3Node* tab_focused_node;
1161
1162 if (target == TabFocus::MouseLocation || mouse != TabFocusMousePriority::Ignore) {
1163 // no surf focused at all
1164 auto ptrSurfaceResource = g_pSeatManager->m_state.pointerFocus.lock();
1165 if (!ptrSurfaceResource) return;
1166
1167 auto ptrSurface = CWLSurface::fromResource(ptrSurfaceResource);
1168 if (!ptrSurface) return;
1169
1170 // non window-parented surface focused, cant have a tab
1171 auto view = ptrSurface->view();
1172 auto* window = dynamic_cast<CWindow*>(view.get());
1173 if (!window || window->isFloating()) return;
1174
1175 auto mouse_pos = g_pInputManager->getMouseCoordsInternal();
1176 tab_node = findTabBarAt(*node, mouse_pos, &tab_focused_node);
1177 if (tab_node != nullptr) goto hastab;
1178
1179 if (target == TabFocus::MouseLocation || mouse == TabFocusMousePriority::Require) return;
1180 }
1181
1182 if (tab_node == nullptr) {
1183 tab_node = this->getWorkspaceFocusedNode(workspace);
1184 if (tab_node == nullptr) return;
1185
1186 while (tab_node != nullptr
1187 && (tab_node->is_target()
1188 || !tab_node->as_group().isTab())
1189 && !tab_node->is_root())
1190 tab_node = tab_node->parent.get();
1191
1192 if (tab_node == nullptr || tab_node->is_target()
1193 || !tab_node->as_group().isTab())
1194 return;
1195 }
1196
1197hastab:
1198 if (target != TabFocus::MouseLocation) {
1199 auto& group = tab_node->as_group();
1200 if (group.focused_child == nullptr || group.children.size() < 2) return;
1201
1202 auto& children = group.children;
1203 if (target == TabFocus::Index) {
1204 int i = 1;
1205
1206 for (auto& n: children) {
1207 if (i == index) {
1208 tab_focused_node = n.get();
1209 goto cont;
1210 }
1211
1212 i++;
1213 }
1214
1215 return;
1216 cont:;
1217 } else {
1218 auto node_iter = group.findChild(*group.focused_child);
1219 if (node_iter == children.end()) return;
1220 if (target == TabFocus::Left) {
1221 if (node_iter == children.begin()) {
1222 if (wrap_scroll) node_iter = std::prev(children.end());
1223 else return;
1224 } else node_iter = std::prev(node_iter);
1225
1226 tab_focused_node = node_iter->get();
1227 } else {
1228 if (node_iter == std::prev(children.end())) {
1229 if (wrap_scroll) node_iter = children.begin();
1230 else return;
1231 } else node_iter = std::next(node_iter);
1232
1233 tab_focused_node = node_iter->get();
1234 }
1235 }
1236 }
1237
1238 auto* focus = tab_focused_node;
1239 while (focus->is_group() && !focus->as_group().group_focused
1240 && focus->as_group().focused_child != nullptr)
1241 focus = focus->as_group().focused_child;
1242
1243 focus->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1244 this->recalcGeometry();
1245}
1246
1247void Hy3Layout::setNodeSwallow(const Workspace::CHLWorkspace* workspace, SetSwallowOption option) {
1248 auto* node = this->getWorkspaceFocusedNode(workspace);
1249 if (node == nullptr) return;
1250 node->assertNotRoot();
1251
1252 auto* containment = &node->parent->as_group().containment;
1253 switch (option) {
1254 case SetSwallowOption::NoSwallow: *containment = false; break;
1255 case SetSwallowOption::Swallow: *containment = true; break;
1256 case SetSwallowOption::Toggle: *containment = !*containment; break;
1257 }
1258}
1259
1260void Hy3Layout::killFocusedNode(const Workspace::CHLWorkspace* workspace) {
1261 auto last_window = Desktop::focusState()->window();
1262 if (last_window != nullptr && last_window->isFloating()) {
1263 last_window->sendClose();
1264 } else {
1265 auto* node = this->getWorkspaceFocusedNode(workspace);
1266 if (node == nullptr) return;
1267
1268 std::vector<PHLWINDOW> windows;
1269 for (auto& w: node->windows()) windows.push_back(w.m_self.lock());
1270
1271 for (auto& window: windows) {
1272 window->setHidden(false);
1273 window->sendClose();
1274 }
1275 }
1276}
1277
1278void Hy3Layout::expand(
1279 const Workspace::CHLWorkspace* workspace,
1280 ExpandOption option,
1281 ExpandFullscreenOption fs_option
1282) {
1283 auto* node = this->getWorkspaceFocusedNode(workspace, false, true);
1284 if (node == nullptr) return;
1285 PHLWINDOW window;
1286
1287 switch (option) {
1288 case ExpandOption::Expand: {
1289 node->assertNotRoot();
1290
1291 if (node->is_group() && !node->as_group().group_focused)
1292 node->as_group().expand_focused = ExpandFocusType::Stack;
1293
1294 auto& group = node->parent->as_group();
1295 group.focused_child = node;
1296 group.expand_focused = ExpandFocusType::Latch;
1297
1298 this->recalcGeometry();
1299
1300 if (node->parent->is_root()) {
1301 switch (fs_option) {
1302 case ExpandFullscreenOption::MaximizeAsFullscreen: // goto fullscreen;
1303 case ExpandFullscreenOption::MaximizeIntermediate:
1304 case ExpandFullscreenOption::MaximizeOnly: return;
1305 }
1306 }
1307 } break;
1308 case ExpandOption::Shrink:
1309 if (node->is_group()) {
1310 auto& group = node->as_group();
1311
1312 group.expand_focused = ExpandFocusType::NotExpanded;
1313 if (group.focused_child->is_group())
1314 group.focused_child->as_group().expand_focused = ExpandFocusType::Latch;
1315
1316 this->recalcGeometry();
1317 }
1318 break;
1319 case ExpandOption::Base: {
1320 if (node->is_group()) {
1321 node->as_group().collapseExpansions();
1322 this->recalcGeometry();
1323 }
1324 break;
1325 }
1326 case ExpandOption::Maximize: break;
1327 case ExpandOption::Fullscreen: break;
1328 }
1329
1330 return;
1331}
1332
1333void Hy3Layout::setTabLock(const Workspace::CHLWorkspace* workspace, TabLockMode mode) {
1334 auto* focused = this->getWorkspaceFocusedNode(workspace);
1335 if (focused == nullptr) return;
1336
1337 for (auto& node: focused->ancestors()) {
1338 auto& group = node.parent->as_group();
1339 if (!group.isTab())
1340 continue;
1341
1342 switch (mode) {
1343 case TabLockMode::Lock: group.locked = true; break;
1344 case TabLockMode::Unlock: group.locked = false; break;
1345 case TabLockMode::Toggle: group.locked = !group.locked; break;
1346 }
1347
1348 node.parent->updateTabBar();
1349 return;
1350 }
1351}
1352
1353static void equalizeRecursive(Hy3Node* node, bool recursive) {
1354 node->size_ratio = 1.0f;
1355
1356 if (recursive && node->is_group()) {
1357 for (auto& child: node->as_group().children) {
1358 equalizeRecursive(child.get(), true);
1359 }
1360 }
1361}
1362
1363void Hy3Layout::equalize(const Workspace::CHLWorkspace* workspace, bool recursive) {
1364 auto* focused = this->getWorkspaceFocusedNode(workspace);
1365 if (focused == nullptr) return;
1366
1367 Hy3Node* target = nullptr;
1368
1369 if (recursive) {
1370 target = this->getWorkspaceRootGroup(workspace);
1371 if (target != nullptr) {
1372 equalizeRecursive(target, true);
1373 }
1374 } else {
1375 focused->assertNotRoot();
1376 auto* parent = focused->parent.get();
1377 equalizeRecursive(parent, false);
1378 target = parent;
1379 }
1380
1381 if (target != nullptr) {
1382 this->recalcGeometry();
1383 }
1384}
1385
1386void Hy3Layout::warpCursorToBox(const Vector2D& pos, const Vector2D& size) {
1387 auto cursorpos = Pointer::mgr()->position();
1388
1389 if (cursorpos.x < pos.x || cursorpos.x >= pos.x + size.x || cursorpos.y < pos.y
1390 || cursorpos.y >= pos.y + size.y)
1391 {
1392 Hy3Layout::warpCursorWithFocus(pos + size / 2, true);
1393 }
1394}
1395
1396void Hy3Layout::warpCursorWithFocus(const Vector2D& target, bool force) {
1397 static const auto input_follows_mouse = CConfigValue<Config::INTEGER>("input:follow_mouse");
1398 static const auto no_warps = CConfigValue<Config::INTEGER>("cursor:no_warps");
1399
1400 Pointer::pointerController()->warpTo(target, force);
1401
1402 if (*no_warps && !force) return;
1403
1404 if (*input_follows_mouse) {
1405 g_pInputManager->simulateMouseMovement();
1406 }
1407}
1408
1409std::string Hy3Layout::debugNodes() {
1410 std::string output;
1411
1412 for (auto* hy3: g_hy3Instances) {
1413 if (!hy3->root) continue;
1414 output += hy3->root->debugNode();
1415 output += "\n";
1416 }
1417
1418 return output;
1419}
1420
1421bool Hy3Layout::shouldRenderSelected(const CWindow* window) {
1422 if (window == nullptr) return false;
1423 if (Desktop::focusState()->window()) return false;
1424
1425 auto* root = this->getWorkspaceRootGroup(window->m_workspace.get());
1426 if (root == nullptr || root->as_group().focused_child == nullptr) return false;
1427 auto* focused = &root->getFocusedNode();
1428
1429 switch (focused->type()) {
1430 case Hy3NodeType::Target: return focused->as_window().get() == window;
1431 case Hy3NodeType::Group: {
1432 auto* node = this->getNodeFromWindow(window);
1433 if (node == nullptr) return false;
1434 return focused->as_group().hasChild(*node);
1435 }
1436 }
1437 return false;
1438}
1439
1440Hy3Node* Hy3Layout::getWorkspaceRootGroup(const Workspace::CHLWorkspace* workspace) {
1441 if (!this->root) return nullptr;
1442 auto& group = this->root->as_group();
1443 if (group.children.empty()) return nullptr;
1444 return group.children.front().get();
1445}
1446
1447Hy3Node* Hy3Layout::getWorkspaceFocusedNode(
1448 const Workspace::CHLWorkspace* workspace,
1449 bool ignore_group_focus,
1450 bool stop_at_expanded
1451) {
1452 auto* rootNode = this->getWorkspaceRootGroup(workspace);
1453 if (rootNode == nullptr) return nullptr;
1454 return &rootNode->getFocusedNode(ignore_group_focus, stop_at_expanded);
1455}
1456
1457Hy3Node* Hy3Layout::getNodeFromWindow(const CWindow* window) {
1458 if (!this->root || !window) return nullptr;
1459 for (auto& w: this->root->windows()) {
1460 if (&w == window) return getNodeFromTarget(w.layoutTarget());
1461 }
1462 return nullptr;
1463}
1464
1465static Hy3Node* findNodeFromTargetRecursive(Hy3Node* node, SP<Layout::ITarget> target) {
1466 if (!node) return nullptr;
1467 if (node->is_target() && node->as_target() == target) {
1468 return node;
1469 }
1470 if (node->is_group()) {
1471 for (auto& child: node->as_group().children) {
1472 auto* result = findNodeFromTargetRecursive(child.get(), target);
1473 if (result) return result;
1474 }
1475 }
1476 return nullptr;
1477}
1478
1479Hy3Node* Hy3Layout::getNodeFromTarget(SP<Layout::ITarget> target) {
1480 return findNodeFromTargetRecursive(this->root.get(), target);
1481}
1482
1483bool shiftIsForward(ShiftDirection direction) {
1484 return direction == ShiftDirection::Right || direction == ShiftDirection::Down;
1485}
1486
1487bool shiftIsVertical(ShiftDirection direction) {
1488 return direction == ShiftDirection::Up || direction == ShiftDirection::Down;
1489}
1490
1491bool shiftMatchesLayout(Hy3GroupLayout layout, ShiftDirection direction) {
1492 if (layout == Hy3GroupLayout::Root) return false;
1493 return (layout == Hy3GroupLayout::SplitV && shiftIsVertical(direction))
1494 || (layout != Hy3GroupLayout::SplitV && !shiftIsVertical(direction));
1495}
1496
1497Hy3Node* Hy3Layout::shiftOrGetFocus(
1498 Hy3Node& node,
1499 ShiftDirection direction,
1500 bool shift,
1501 bool once,
1502 bool visible
1503) {
1504 auto* expand_actor = &node.getExpandActor();
1505 auto* break_origin = &expand_actor->getPlacementActor();
1506 auto* shift_actor = break_origin;
1507 auto* break_parent = break_origin->parent.get();
1508
1509 auto has_broken_once = false;
1510
1511 // break parents until we hit a container oriented the same way as the shift
1512 // direction
1513 while (true) {
1514 if (break_parent == nullptr) return nullptr;
1515
1516 auto& group = break_parent->as_group(); // must be a group in order to be a parent
1517
1518 if (shiftMatchesLayout(group.layout, direction)
1519 && (!visible || !group.isTab()))
1520 {
1521 // group has the correct orientation
1522
1523 if (once && shift && has_broken_once) break;
1524 if (break_origin != shift_actor) has_broken_once = true;
1525
1526 // if this movement would break out of the group, continue the break loop
1527 // (do not enter this if) otherwise break.
1528 if ((has_broken_once && once && shift)
1529 || !(
1530 (!shiftIsForward(direction) && group.children.front().get() == break_origin)
1531 || (shiftIsForward(direction) && group.children.back().get() == break_origin)
1532 ))
1533 break;
1534 }
1535
1536 if (break_parent->is_root()) {
1537 if (!shift) return focusMonitor(direction);
1538
1539 auto new_layout =
1540 shiftIsVertical(direction) ? Hy3GroupLayout::SplitV : Hy3GroupLayout::SplitH;
1541 break_origin->wrap(new_layout, GroupEphemeralityOption::Standard);
1542 break_parent = break_origin->parent.get();
1543 break;
1544 }
1545
1546 // special case 1-child nodes so once will only break the group
1547 if (once && shift && break_origin->is_group() && break_origin->as_group().children.size() == 1) {
1548 break;
1549 }
1550
1551 break_origin = break_parent;
1552 break_parent = break_origin->parent.get();
1553 }
1554
1555 auto& parent_group = break_parent->as_group();
1556 Hy3Node* target_group = break_parent;
1557 std::list<UP<Hy3Node>>::iterator insert;
1558
1559 if (break_origin == parent_group.children.front().get() && !shiftIsForward(direction)) {
1560 if (!shift) return nullptr;
1561 insert = parent_group.children.begin();
1562 } else if (break_origin == parent_group.children.back().get() && shiftIsForward(direction)) {
1563 if (!shift) return nullptr;
1564 insert = parent_group.children.end();
1565 } else {
1566 auto& group_data = target_group->as_group();
1567
1568 auto iter = group_data.findChild(*break_origin);
1569 if (shiftIsForward(direction)) iter = std::next(iter);
1570 else iter = std::prev(iter);
1571
1572 auto& node = **iter;
1573 if (node.is_target()
1574 || (node.is_group()
1575 && (node.as_group().expand_focused != ExpandFocusType::NotExpanded
1576 || node.as_group().locked))
1577 || (shift && once && has_broken_once))
1578 {
1579 if (shift) {
1580 if (target_group == shift_actor->parent.get()) {
1581 if (shiftIsForward(direction)) insert = std::next(iter);
1582 else insert = iter;
1583 } else {
1584 if (shiftIsForward(direction)) insert = iter;
1585 else insert = std::next(iter);
1586 }
1587 } else return &(*iter)->getFocusedNode();
1588 } else {
1589 // break into neighboring groups until we hit a window
1590 while (true) {
1591 target_group = iter->get();
1592 auto& group_data = target_group->as_group();
1593
1594 if (group_data.children.empty()) return nullptr; // in theory this would never happen
1595
1596 bool shift_after = false;
1597
1598 if (!shift && group_data.isTab()
1599 && group_data.focused_child != nullptr)
1600 {
1601 iter = group_data.findChild(*group_data.focused_child);
1602 } else if (visible && group_data.isTab()
1603 && group_data.focused_child != nullptr)
1604 {
1605 // if the group is tabbed and we're going by visible nodes, jump to the current entry
1606 iter = group_data.findChild(*group_data.focused_child);
1607 shift_after = true;
1608 } else if (shiftMatchesLayout(group_data.layout, direction)
1609 || (visible && group_data.isTab()))
1610 {
1611 // if the group has the same orientation as movement pick the
1612 // last/first child based on movement direction
1613 if (shiftIsForward(direction)) iter = group_data.children.begin();
1614 else {
1615 iter = std::prev(group_data.children.end());
1616 shift_after = true;
1617 }
1618 } else {
1619 if (group_data.focused_child != nullptr) {
1620 iter = group_data.findChild(*group_data.focused_child);
1621 shift_after = true;
1622 } else {
1623 iter = group_data.children.begin();
1624 }
1625 }
1626
1627 if (shift && once) {
1628 if (shift_after) insert = std::next(iter);
1629 else insert = iter;
1630 break;
1631 }
1632
1633 if ((*iter)->is_target()
1634 || ((*iter)->is_group()
1635 && (*iter)->as_group().expand_focused != ExpandFocusType::NotExpanded))
1636 {
1637 if (shift) {
1638 if (shift_after) insert = std::next(iter);
1639 else insert = iter;
1640 break;
1641 } else {
1642 return &(*iter)->getFocusedNode();
1643 }
1644 }
1645 }
1646 }
1647 }
1648
1649 auto& group_data = target_group->as_group();
1650
1651 if (target_group == shift_actor->parent.get()) {
1652 // Reorder within the same group via splice (handles boundary no-ops naturally)
1653 auto shift_it = group_data.findChild(*shift_actor);
1654 group_data.children.splice(insert, group_data.children, shift_it);
1655 shift_actor->parent->collapseParents(nodeCollapsePolicy());
1656 } else if (!shift_actor->parent->is_root() && shift_actor->parent->as_group().children.size() == 1 && target_group == shift_actor->parent->parent.get()) {
1657 // special cased to prevent size being reset to 1 on group break
1658 auto shift_parent = shift_actor->parent;
1659 auto shift_actor_u = shift_parent->as_group().extractChildRaw(*shift_actor);
1660 auto iter = std::ranges::find_if(group_data.children, [&](const auto& other) {
1661 return other == shift_parent;
1662 });
1663 group_data.replaceChild(iter, std::move(shift_actor_u));
1664 } else {
1665 auto target_group_p = target_group->self;
1666 auto* shift_parent = shift_actor->parent.get();
1667 auto shift_actor_u = shift_parent->as_group().extractChild(*shift_actor);
1668
1669 group_data.insertChild(insert, std::move(shift_actor_u));
1670
1671 shift_parent = shift_parent->collapseParents(CollapsePolicy::InvalidOnly);
1672
1673 if (shift_parent != nullptr) {
1674 shift_parent->updateTabBarRecursive();
1675 }
1676
1677 // Collapse any single-child groups left over from wrapping/extraction
1678 if (target_group_p) {
1679 target_group_p->collapseParents(nodeCollapsePolicy());
1680 }
1681 }
1682
1683 node.updateTabBarRecursive();
1684 node.focus(false, Desktop::FOCUS_REASON_KEYBIND);
1685 this->recalcGeometry();
1686
1687 return nullptr;
1688}
1689
1690void Hy3Layout::updateAutotileWorkspaces() {
1691 static const auto autotile_raw_workspaces =
1692 CConfigValue<Config::STRING>("plugin:hy3:autotile:workspaces");
1693
1694 if (*autotile_raw_workspaces == this->autotile.raw_workspaces) {
1695 return;
1696 }
1697
1698 this->autotile.raw_workspaces = *autotile_raw_workspaces;
1699 this->autotile.workspaces.clear();
1700
1701 if (this->autotile.raw_workspaces == "all") {
1702 return;
1703 }
1704
1705 this->autotile.workspace_blacklist = this->autotile.raw_workspaces.rfind("not:", 0) == 0;
1706
1707 const auto autotile_raw_workspaces_filtered = (this->autotile.workspace_blacklist)
1708 ? this->autotile.raw_workspaces.substr(4)
1709 : this->autotile.raw_workspaces;
1710
1711 // split on space and comma
1712 const std::regex regex {R"([\s,]+)"};
1713 const auto begin = std::sregex_token_iterator(
1714 autotile_raw_workspaces_filtered.begin(),
1715 autotile_raw_workspaces_filtered.end(),
1716 regex,
1717 -1
1718 );
1719 const auto end = std::sregex_token_iterator();
1720
1721 for (auto s = begin; s != end; ++s) {
1722 try {
1723 this->autotile.workspaces.insert(std::stoi(*s));
1724 } catch (...) {
1725 hy3_log(ERR, "autotile:workspaces: invalid workspace id: {}", (std::string) *s);
1726 }
1727 }
1728}
1729
1730bool Hy3Layout::shouldAutotileWorkspace(const Workspace::CHLWorkspace* workspace) {
1731 if (this->autotile.workspace_blacklist) {
1732 return !this->autotile.workspaces.contains(static_cast<int>(workspace->numberedID().value_or(0)));
1733 } else {
1734 return this->autotile.workspaces.empty()
1735 || this->autotile.workspaces.contains(static_cast<int>(workspace->numberedID().value_or(0)));
1736 }
1737}