Hy3Layout.cpp
51109 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/desktop/Workspace.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()->m_id : -1
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 id {}",
212 (uintptr_t) node_up.get(),
213 ws ? ws->m_id : -1
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->m_id);
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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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 CWorkspace* 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->m_name, follow, false);
885 return true;
886 }
887 }
888 return false;
889}
890
891void Hy3Layout::toggleFocusLayer(const CWorkspace* 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 CWorkspace* origin,
940 std::string wsname,
941 bool follow,
942 bool warp
943) {
944 auto target = getWorkspaceIDNameFromString(operationWorkspaceForName(wsname));
945
946 if (target.id == WORKSPACE_INVALID) {
947 hy3_log(ERR, "moveNodeToWorkspace called with invalid workspace {}", wsname);
948 return;
949 }
950
951 auto workspace = State::workspaceState()->query().id(target.id).run();
952
953 if (origin == workspace.get()) return;
954
955 auto* node = this->getWorkspaceFocusedNode(origin);
956 auto focused_window = Desktop::focusState()->window();
957 auto* focused_window_node = this->getNodeFromWindow(focused_window.get());
958
959 auto origin_ws = node != nullptr ? node->layout()->workspace()
960 : focused_window != nullptr ? focused_window->m_workspace
961 : nullptr;
962
963 if (!valid(origin_ws)) return;
964
965 if (workspace == nullptr) {
966 hy3_log(LOG, "creating target workspace {} for node move", target.id);
967
968 workspace = State::workspaceState()->create(target.id, origin_ws->monitorID(), target.name);
969 }
970
971 if (focused_window != nullptr
972 && (focused_window_node == nullptr || Fullscreen::controller()->isFullscreen(focused_window)))
973 {
974 g_pHyprRenderer->damageWindow(focused_window);
975 Desktop::globalWindowController()->moveWindowToWorkspace(focused_window, workspace);
976 } else {
977 if (node == nullptr) return;
978
979 hy3_log(
980 LOG,
981 "moving node {:x} from workspace {} to workspace {} (follow: {})",
982 (uintptr_t) node,
983 origin->m_id,
984 workspace->m_id,
985 follow
986 );
987
988 auto* parent_node = node->parent.get();
989 auto node_up = parent_node->extractAndMerge(*node, nullptr);
990 auto* destHy3 = hy3InstanceForWorkspace(workspace);
991 auto* destLayout = destHy3 ? destHy3 : this;
992
993 g_suppressInsert = true;
994
995 for (auto& window: node->windows()) {
996 window.layoutTarget()->assignToSpace(workspace->m_space);
997 }
998
999 g_suppressInsert = false;
1000
1001 destLayout->insertNode(std::move(node_up));
1002
1003 Desktop::Rule::ruleEngine()->updateAllRules();
1004
1005 updateTreeTabBars(*node);
1006 node->updateTabBarRecursive();
1007 this->recalcGeometry();
1008 }
1009
1010 if (follow) {
1011 auto& monitor = workspace->m_monitor;
1012
1013 if (workspace->m_isSpecialWorkspace) {
1014 monitor->setSpecialWorkspace(workspace);
1015 } else if (origin_ws->m_isSpecialWorkspace) {
1016 origin_ws->m_monitor->setSpecialWorkspace(nullptr);
1017 }
1018
1019 monitor->changeWorkspace(workspace);
1020
1021 node->layout()->recalcGeometry();
1022 node->focus(warp, Desktop::FOCUS_REASON_KEYBIND);
1023 }
1024}
1025
1026void Hy3Layout::changeFocus(const CWorkspace* workspace, FocusShift shift) {
1027 auto* node = this->getWorkspaceFocusedNode(workspace);
1028 if (node == nullptr) return;
1029
1030 switch (shift) {
1031 case FocusShift::Bottom: goto bottom;
1032 case FocusShift::Top:
1033 this->root->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1034 this->updateGroupBorderColors();
1035 return;
1036 case FocusShift::Raise:
1037 if (node->is_root_group()) goto bottom;
1038 node->parent->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1039 this->updateGroupBorderColors();
1040 return;
1041 case FocusShift::Lower:
1042 if (node->is_group() && node->as_group().focused_child != nullptr)
1043 node->as_group().focused_child->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1044 this->updateGroupBorderColors();
1045 return;
1046 case FocusShift::Tab:
1047 for (auto& n: node->ancestors()) {
1048 if (n.parent->as_group().isTab()) {
1049 n.parent->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1050 this->updateGroupBorderColors();
1051 return;
1052 }
1053 }
1054 return;
1055 case FocusShift::TabNode:
1056 for (auto& n: node->ancestors()) {
1057 if (n.parent->as_group().isTab()) {
1058 n.focus(false, Desktop::FOCUS_REASON_KEYBIND);
1059 this->updateGroupBorderColors();
1060 return;
1061 }
1062 }
1063 return;
1064 }
1065
1066bottom:
1067 while (node->is_group() && node->as_group().focused_child != nullptr) {
1068 node = node->as_group().focused_child;
1069 }
1070
1071 node->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1072 this->updateGroupBorderColors();
1073 return;
1074}
1075
1076Hy3Node* findTabBarAt(Hy3Node& node, Vector2D pos, Hy3Node** focused_node) {
1077 // clang-format off
1078 static const auto p_gaps_in = CConfigValue<Config::IComplexConfigValue>("general:gaps_in");
1079 static const auto tab_bar_height = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:height");
1080 static const auto tab_bar_padding = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding");
1081 // clang-format on
1082
1083 auto workspace_rule = Config::workspaceRuleMgr()->getWorkspaceRuleFor(node.layout()->workspace());
1084 auto gaps_in = workspace_rule.and_then([](auto r) { return r.m_gapsIn; }).value_or(*sc<Config::CCssGapData*>(p_gaps_in.ptr()));
1085
1086 auto inset = *tab_bar_height + *tab_bar_padding + gaps_in.m_top;
1087
1088 if (node.is_group()) {
1089 if (node.hidden) return nullptr;
1090 // note: tab bar clicks ignore animations
1091 if (node.visualBox.x > pos.x || node.logicalBox.y > pos.y || node.visualBox.x + node.visualBox.w < pos.x
1092 || node.visualBox.y + node.visualBox.h < pos.y)
1093 return nullptr;
1094
1095 auto& group = node.as_group();
1096
1097 if (group.isTab() && group.tab_bar) {
1098 if (pos.y < node.visualBox.y + inset) {
1099 auto& children = group.children;
1100 auto& tab_bar = *group.tab_bar.get();
1101
1102 auto size = tab_bar.size->value();
1103 auto x = pos.x - tab_bar.pos->value().x;
1104 auto child_iter = children.begin();
1105
1106 for (auto& tab: tab_bar.bar.entries) {
1107 if (child_iter == children.end()) break;
1108
1109 if (x > tab.offset->value() * size.x
1110 && x < (tab.offset->value() + tab.width->value()) * size.x)
1111 {
1112 *focused_node = child_iter->get();
1113 return &node;
1114 }
1115
1116 child_iter = std::next(child_iter);
1117 }
1118 }
1119
1120 if (group.focused_child != nullptr) {
1121 return findTabBarAt(*group.focused_child, pos, focused_node);
1122 }
1123 } else {
1124 for (auto& child: group.children) {
1125 if (findTabBarAt(*child, pos, focused_node)) return child.get();
1126 }
1127 }
1128 }
1129
1130 return nullptr;
1131}
1132
1133void Hy3Layout::focusTab(
1134 const CWorkspace* workspace,
1135 TabFocus target,
1136 TabFocusMousePriority mouse,
1137 bool wrap_scroll,
1138 int index
1139) {
1140 auto* node = this->getWorkspaceRootGroup(workspace);
1141 if (node == nullptr) return;
1142
1143 Hy3Node* tab_node = nullptr;
1144 Hy3Node* tab_focused_node;
1145
1146 if (target == TabFocus::MouseLocation || mouse != TabFocusMousePriority::Ignore) {
1147 // no surf focused at all
1148 auto ptrSurfaceResource = g_pSeatManager->m_state.pointerFocus.lock();
1149 if (!ptrSurfaceResource) return;
1150
1151 auto ptrSurface = CWLSurface::fromResource(ptrSurfaceResource);
1152 if (!ptrSurface) return;
1153
1154 // non window-parented surface focused, cant have a tab
1155 auto view = ptrSurface->view();
1156 auto* window = dynamic_cast<CWindow*>(view.get());
1157 if (!window || window->isFloating()) return;
1158
1159 auto mouse_pos = g_pInputManager->getMouseCoordsInternal();
1160 tab_node = findTabBarAt(*node, mouse_pos, &tab_focused_node);
1161 if (tab_node != nullptr) goto hastab;
1162
1163 if (target == TabFocus::MouseLocation || mouse == TabFocusMousePriority::Require) return;
1164 }
1165
1166 if (tab_node == nullptr) {
1167 tab_node = this->getWorkspaceFocusedNode(workspace);
1168 if (tab_node == nullptr) return;
1169
1170 while (tab_node != nullptr
1171 && (tab_node->is_target()
1172 || !tab_node->as_group().isTab())
1173 && !tab_node->is_root())
1174 tab_node = tab_node->parent.get();
1175
1176 if (tab_node == nullptr || tab_node->is_target()
1177 || !tab_node->as_group().isTab())
1178 return;
1179 }
1180
1181hastab:
1182 if (target != TabFocus::MouseLocation) {
1183 auto& group = tab_node->as_group();
1184 if (group.focused_child == nullptr || group.children.size() < 2) return;
1185
1186 auto& children = group.children;
1187 if (target == TabFocus::Index) {
1188 int i = 1;
1189
1190 for (auto& n: children) {
1191 if (i == index) {
1192 tab_focused_node = n.get();
1193 goto cont;
1194 }
1195
1196 i++;
1197 }
1198
1199 return;
1200 cont:;
1201 } else {
1202 auto node_iter = group.findChild(*group.focused_child);
1203 if (node_iter == children.end()) return;
1204 if (target == TabFocus::Left) {
1205 if (node_iter == children.begin()) {
1206 if (wrap_scroll) node_iter = std::prev(children.end());
1207 else return;
1208 } else node_iter = std::prev(node_iter);
1209
1210 tab_focused_node = node_iter->get();
1211 } else {
1212 if (node_iter == std::prev(children.end())) {
1213 if (wrap_scroll) node_iter = children.begin();
1214 else return;
1215 } else node_iter = std::next(node_iter);
1216
1217 tab_focused_node = node_iter->get();
1218 }
1219 }
1220 }
1221
1222 auto* focus = tab_focused_node;
1223 while (focus->is_group() && !focus->as_group().group_focused
1224 && focus->as_group().focused_child != nullptr)
1225 focus = focus->as_group().focused_child;
1226
1227 focus->focus(false, Desktop::FOCUS_REASON_KEYBIND);
1228 this->recalcGeometry();
1229}
1230
1231void Hy3Layout::setNodeSwallow(const CWorkspace* workspace, SetSwallowOption option) {
1232 auto* node = this->getWorkspaceFocusedNode(workspace);
1233 if (node == nullptr) return;
1234 node->assertNotRoot();
1235
1236 auto* containment = &node->parent->as_group().containment;
1237 switch (option) {
1238 case SetSwallowOption::NoSwallow: *containment = false; break;
1239 case SetSwallowOption::Swallow: *containment = true; break;
1240 case SetSwallowOption::Toggle: *containment = !*containment; break;
1241 }
1242}
1243
1244void Hy3Layout::killFocusedNode(const CWorkspace* workspace) {
1245 auto last_window = Desktop::focusState()->window();
1246 if (last_window != nullptr && last_window->isFloating()) {
1247 last_window->sendClose();
1248 } else {
1249 auto* node = this->getWorkspaceFocusedNode(workspace);
1250 if (node == nullptr) return;
1251
1252 std::vector<PHLWINDOW> windows;
1253 for (auto& w: node->windows()) windows.push_back(w.m_self.lock());
1254
1255 for (auto& window: windows) {
1256 window->setHidden(false);
1257 window->sendClose();
1258 }
1259 }
1260}
1261
1262void Hy3Layout::expand(
1263 const CWorkspace* workspace,
1264 ExpandOption option,
1265 ExpandFullscreenOption fs_option
1266) {
1267 auto* node = this->getWorkspaceFocusedNode(workspace, false, true);
1268 if (node == nullptr) return;
1269 PHLWINDOW window;
1270
1271 switch (option) {
1272 case ExpandOption::Expand: {
1273 node->assertNotRoot();
1274
1275 if (node->is_group() && !node->as_group().group_focused)
1276 node->as_group().expand_focused = ExpandFocusType::Stack;
1277
1278 auto& group = node->parent->as_group();
1279 group.focused_child = node;
1280 group.expand_focused = ExpandFocusType::Latch;
1281
1282 this->recalcGeometry();
1283
1284 if (node->parent->is_root()) {
1285 switch (fs_option) {
1286 case ExpandFullscreenOption::MaximizeAsFullscreen: // goto fullscreen;
1287 case ExpandFullscreenOption::MaximizeIntermediate:
1288 case ExpandFullscreenOption::MaximizeOnly: return;
1289 }
1290 }
1291 } break;
1292 case ExpandOption::Shrink:
1293 if (node->is_group()) {
1294 auto& group = node->as_group();
1295
1296 group.expand_focused = ExpandFocusType::NotExpanded;
1297 if (group.focused_child->is_group())
1298 group.focused_child->as_group().expand_focused = ExpandFocusType::Latch;
1299
1300 this->recalcGeometry();
1301 }
1302 break;
1303 case ExpandOption::Base: {
1304 if (node->is_group()) {
1305 node->as_group().collapseExpansions();
1306 this->recalcGeometry();
1307 }
1308 break;
1309 }
1310 case ExpandOption::Maximize: break;
1311 case ExpandOption::Fullscreen: break;
1312 }
1313
1314 return;
1315}
1316
1317void Hy3Layout::setTabLock(const CWorkspace* workspace, TabLockMode mode) {
1318 auto* focused = this->getWorkspaceFocusedNode(workspace);
1319 if (focused == nullptr) return;
1320
1321 for (auto& node: focused->ancestors()) {
1322 auto& group = node.parent->as_group();
1323 if (!group.isTab())
1324 continue;
1325
1326 switch (mode) {
1327 case TabLockMode::Lock: group.locked = true; break;
1328 case TabLockMode::Unlock: group.locked = false; break;
1329 case TabLockMode::Toggle: group.locked = !group.locked; break;
1330 }
1331
1332 node.parent->updateTabBar();
1333 return;
1334 }
1335}
1336
1337static void equalizeRecursive(Hy3Node* node, bool recursive) {
1338 node->size_ratio = 1.0f;
1339
1340 if (recursive && node->is_group()) {
1341 for (auto& child: node->as_group().children) {
1342 equalizeRecursive(child.get(), true);
1343 }
1344 }
1345}
1346
1347void Hy3Layout::equalize(const CWorkspace* workspace, bool recursive) {
1348 auto* focused = this->getWorkspaceFocusedNode(workspace);
1349 if (focused == nullptr) return;
1350
1351 Hy3Node* target = nullptr;
1352
1353 if (recursive) {
1354 target = this->getWorkspaceRootGroup(workspace);
1355 if (target != nullptr) {
1356 equalizeRecursive(target, true);
1357 }
1358 } else {
1359 focused->assertNotRoot();
1360 auto* parent = focused->parent.get();
1361 equalizeRecursive(parent, false);
1362 target = parent;
1363 }
1364
1365 if (target != nullptr) {
1366 this->recalcGeometry();
1367 }
1368}
1369
1370void Hy3Layout::warpCursorToBox(const Vector2D& pos, const Vector2D& size) {
1371 auto cursorpos = Pointer::mgr()->position();
1372
1373 if (cursorpos.x < pos.x || cursorpos.x >= pos.x + size.x || cursorpos.y < pos.y
1374 || cursorpos.y >= pos.y + size.y)
1375 {
1376 Hy3Layout::warpCursorWithFocus(pos + size / 2, true);
1377 }
1378}
1379
1380void Hy3Layout::warpCursorWithFocus(const Vector2D& target, bool force) {
1381 static const auto input_follows_mouse = CConfigValue<Config::INTEGER>("input:follow_mouse");
1382 static const auto no_warps = CConfigValue<Config::INTEGER>("cursor:no_warps");
1383
1384 Pointer::pointerController()->warpTo(target, force);
1385
1386 if (*no_warps && !force) return;
1387
1388 if (*input_follows_mouse) {
1389 g_pInputManager->simulateMouseMovement();
1390 }
1391}
1392
1393std::string Hy3Layout::debugNodes() {
1394 std::string output;
1395
1396 for (auto* hy3: g_hy3Instances) {
1397 if (!hy3->root) continue;
1398 output += hy3->root->debugNode();
1399 output += "\n";
1400 }
1401
1402 return output;
1403}
1404
1405bool Hy3Layout::shouldRenderSelected(const CWindow* window) {
1406 if (window == nullptr) return false;
1407 if (Desktop::focusState()->window()) return false;
1408
1409 auto* root = this->getWorkspaceRootGroup(window->m_workspace.get());
1410 if (root == nullptr || root->as_group().focused_child == nullptr) return false;
1411 auto* focused = &root->getFocusedNode();
1412
1413 switch (focused->type()) {
1414 case Hy3NodeType::Target: return focused->as_window().get() == window;
1415 case Hy3NodeType::Group: {
1416 auto* node = this->getNodeFromWindow(window);
1417 if (node == nullptr) return false;
1418 return focused->as_group().hasChild(*node);
1419 }
1420 }
1421 return false;
1422}
1423
1424Hy3Node* Hy3Layout::getWorkspaceRootGroup(const CWorkspace* workspace) {
1425 if (!this->root) return nullptr;
1426 auto& group = this->root->as_group();
1427 if (group.children.empty()) return nullptr;
1428 return group.children.front().get();
1429}
1430
1431Hy3Node* Hy3Layout::getWorkspaceFocusedNode(
1432 const CWorkspace* workspace,
1433 bool ignore_group_focus,
1434 bool stop_at_expanded
1435) {
1436 auto* rootNode = this->getWorkspaceRootGroup(workspace);
1437 if (rootNode == nullptr) return nullptr;
1438 return &rootNode->getFocusedNode(ignore_group_focus, stop_at_expanded);
1439}
1440
1441Hy3Node* Hy3Layout::getNodeFromWindow(const CWindow* window) {
1442 if (!this->root || !window) return nullptr;
1443 for (auto& w: this->root->windows()) {
1444 if (&w == window) return getNodeFromTarget(w.layoutTarget());
1445 }
1446 return nullptr;
1447}
1448
1449static Hy3Node* findNodeFromTargetRecursive(Hy3Node* node, SP<Layout::ITarget> target) {
1450 if (!node) return nullptr;
1451 if (node->is_target() && node->as_target() == target) {
1452 return node;
1453 }
1454 if (node->is_group()) {
1455 for (auto& child: node->as_group().children) {
1456 auto* result = findNodeFromTargetRecursive(child.get(), target);
1457 if (result) return result;
1458 }
1459 }
1460 return nullptr;
1461}
1462
1463Hy3Node* Hy3Layout::getNodeFromTarget(SP<Layout::ITarget> target) {
1464 return findNodeFromTargetRecursive(this->root.get(), target);
1465}
1466
1467bool shiftIsForward(ShiftDirection direction) {
1468 return direction == ShiftDirection::Right || direction == ShiftDirection::Down;
1469}
1470
1471bool shiftIsVertical(ShiftDirection direction) {
1472 return direction == ShiftDirection::Up || direction == ShiftDirection::Down;
1473}
1474
1475bool shiftMatchesLayout(Hy3GroupLayout layout, ShiftDirection direction) {
1476 if (layout == Hy3GroupLayout::Root) return false;
1477 return (layout == Hy3GroupLayout::SplitV && shiftIsVertical(direction))
1478 || (layout != Hy3GroupLayout::SplitV && !shiftIsVertical(direction));
1479}
1480
1481Hy3Node* Hy3Layout::shiftOrGetFocus(
1482 Hy3Node& node,
1483 ShiftDirection direction,
1484 bool shift,
1485 bool once,
1486 bool visible
1487) {
1488 auto* expand_actor = &node.getExpandActor();
1489 auto* break_origin = &expand_actor->getPlacementActor();
1490 auto* shift_actor = break_origin;
1491 auto* break_parent = break_origin->parent.get();
1492
1493 auto has_broken_once = false;
1494
1495 // break parents until we hit a container oriented the same way as the shift
1496 // direction
1497 while (true) {
1498 if (break_parent == nullptr) return nullptr;
1499
1500 auto& group = break_parent->as_group(); // must be a group in order to be a parent
1501
1502 if (shiftMatchesLayout(group.layout, direction)
1503 && (!visible || !group.isTab()))
1504 {
1505 // group has the correct orientation
1506
1507 if (once && shift && has_broken_once) break;
1508 if (break_origin != shift_actor) has_broken_once = true;
1509
1510 // if this movement would break out of the group, continue the break loop
1511 // (do not enter this if) otherwise break.
1512 if ((has_broken_once && once && shift)
1513 || !(
1514 (!shiftIsForward(direction) && group.children.front().get() == break_origin)
1515 || (shiftIsForward(direction) && group.children.back().get() == break_origin)
1516 ))
1517 break;
1518 }
1519
1520 if (break_parent->is_root()) {
1521 if (!shift) return focusMonitor(direction);
1522
1523 auto new_layout =
1524 shiftIsVertical(direction) ? Hy3GroupLayout::SplitV : Hy3GroupLayout::SplitH;
1525 break_origin->wrap(new_layout, GroupEphemeralityOption::Standard);
1526 break_parent = break_origin->parent.get();
1527 break;
1528 }
1529
1530 // special case 1-child nodes so once will only break the group
1531 if (once && shift && break_origin->is_group() && break_origin->as_group().children.size() == 1) {
1532 break;
1533 }
1534
1535 break_origin = break_parent;
1536 break_parent = break_origin->parent.get();
1537 }
1538
1539 auto& parent_group = break_parent->as_group();
1540 Hy3Node* target_group = break_parent;
1541 std::list<UP<Hy3Node>>::iterator insert;
1542
1543 if (break_origin == parent_group.children.front().get() && !shiftIsForward(direction)) {
1544 if (!shift) return nullptr;
1545 insert = parent_group.children.begin();
1546 } else if (break_origin == parent_group.children.back().get() && shiftIsForward(direction)) {
1547 if (!shift) return nullptr;
1548 insert = parent_group.children.end();
1549 } else {
1550 auto& group_data = target_group->as_group();
1551
1552 auto iter = group_data.findChild(*break_origin);
1553 if (shiftIsForward(direction)) iter = std::next(iter);
1554 else iter = std::prev(iter);
1555
1556 auto& node = **iter;
1557 if (node.is_target()
1558 || (node.is_group()
1559 && (node.as_group().expand_focused != ExpandFocusType::NotExpanded
1560 || node.as_group().locked))
1561 || (shift && once && has_broken_once))
1562 {
1563 if (shift) {
1564 if (target_group == shift_actor->parent.get()) {
1565 if (shiftIsForward(direction)) insert = std::next(iter);
1566 else insert = iter;
1567 } else {
1568 if (shiftIsForward(direction)) insert = iter;
1569 else insert = std::next(iter);
1570 }
1571 } else return &(*iter)->getFocusedNode();
1572 } else {
1573 // break into neighboring groups until we hit a window
1574 while (true) {
1575 target_group = iter->get();
1576 auto& group_data = target_group->as_group();
1577
1578 if (group_data.children.empty()) return nullptr; // in theory this would never happen
1579
1580 bool shift_after = false;
1581
1582 if (!shift && group_data.isTab()
1583 && group_data.focused_child != nullptr)
1584 {
1585 iter = group_data.findChild(*group_data.focused_child);
1586 } else if (visible && group_data.isTab()
1587 && group_data.focused_child != nullptr)
1588 {
1589 // if the group is tabbed and we're going by visible nodes, jump to the current entry
1590 iter = group_data.findChild(*group_data.focused_child);
1591 shift_after = true;
1592 } else if (shiftMatchesLayout(group_data.layout, direction)
1593 || (visible && group_data.isTab()))
1594 {
1595 // if the group has the same orientation as movement pick the
1596 // last/first child based on movement direction
1597 if (shiftIsForward(direction)) iter = group_data.children.begin();
1598 else {
1599 iter = std::prev(group_data.children.end());
1600 shift_after = true;
1601 }
1602 } else {
1603 if (group_data.focused_child != nullptr) {
1604 iter = group_data.findChild(*group_data.focused_child);
1605 shift_after = true;
1606 } else {
1607 iter = group_data.children.begin();
1608 }
1609 }
1610
1611 if (shift && once) {
1612 if (shift_after) insert = std::next(iter);
1613 else insert = iter;
1614 break;
1615 }
1616
1617 if ((*iter)->is_target()
1618 || ((*iter)->is_group()
1619 && (*iter)->as_group().expand_focused != ExpandFocusType::NotExpanded))
1620 {
1621 if (shift) {
1622 if (shift_after) insert = std::next(iter);
1623 else insert = iter;
1624 break;
1625 } else {
1626 return &(*iter)->getFocusedNode();
1627 }
1628 }
1629 }
1630 }
1631 }
1632
1633 auto& group_data = target_group->as_group();
1634
1635 if (target_group == shift_actor->parent.get()) {
1636 // Reorder within the same group via splice (handles boundary no-ops naturally)
1637 auto shift_it = group_data.findChild(*shift_actor);
1638 group_data.children.splice(insert, group_data.children, shift_it);
1639 shift_actor->parent->collapseParents(nodeCollapsePolicy());
1640 } else if (!shift_actor->parent->is_root() && shift_actor->parent->as_group().children.size() == 1 && target_group == shift_actor->parent->parent.get()) {
1641 // special cased to prevent size being reset to 1 on group break
1642 auto shift_parent = shift_actor->parent;
1643 auto shift_actor_u = shift_parent->as_group().extractChildRaw(*shift_actor);
1644 auto iter = std::ranges::find_if(group_data.children, [&](const auto& other) {
1645 return other == shift_parent;
1646 });
1647 group_data.replaceChild(iter, std::move(shift_actor_u));
1648 } else {
1649 auto target_group_p = target_group->self;
1650 auto* shift_parent = shift_actor->parent.get();
1651 auto shift_actor_u = shift_parent->as_group().extractChild(*shift_actor);
1652
1653 group_data.insertChild(insert, std::move(shift_actor_u));
1654
1655 shift_parent = shift_parent->collapseParents(CollapsePolicy::InvalidOnly);
1656
1657 if (shift_parent != nullptr) {
1658 shift_parent->updateTabBarRecursive();
1659 }
1660
1661 // Collapse any single-child groups left over from wrapping/extraction
1662 if (target_group_p) {
1663 target_group_p->collapseParents(nodeCollapsePolicy());
1664 }
1665 }
1666
1667 node.updateTabBarRecursive();
1668 node.focus(false, Desktop::FOCUS_REASON_KEYBIND);
1669 this->recalcGeometry();
1670
1671 return nullptr;
1672}
1673
1674void Hy3Layout::updateAutotileWorkspaces() {
1675 static const auto autotile_raw_workspaces =
1676 CConfigValue<Config::STRING>("plugin:hy3:autotile:workspaces");
1677
1678 if (*autotile_raw_workspaces == this->autotile.raw_workspaces) {
1679 return;
1680 }
1681
1682 this->autotile.raw_workspaces = *autotile_raw_workspaces;
1683 this->autotile.workspaces.clear();
1684
1685 if (this->autotile.raw_workspaces == "all") {
1686 return;
1687 }
1688
1689 this->autotile.workspace_blacklist = this->autotile.raw_workspaces.rfind("not:", 0) == 0;
1690
1691 const auto autotile_raw_workspaces_filtered = (this->autotile.workspace_blacklist)
1692 ? this->autotile.raw_workspaces.substr(4)
1693 : this->autotile.raw_workspaces;
1694
1695 // split on space and comma
1696 const std::regex regex {R"([\s,]+)"};
1697 const auto begin = std::sregex_token_iterator(
1698 autotile_raw_workspaces_filtered.begin(),
1699 autotile_raw_workspaces_filtered.end(),
1700 regex,
1701 -1
1702 );
1703 const auto end = std::sregex_token_iterator();
1704
1705 for (auto s = begin; s != end; ++s) {
1706 try {
1707 this->autotile.workspaces.insert(std::stoi(*s));
1708 } catch (...) {
1709 hy3_log(ERR, "autotile:workspaces: invalid workspace id: {}", (std::string) *s);
1710 }
1711 }
1712}
1713
1714bool Hy3Layout::shouldAutotileWorkspace(const CWorkspace* workspace) {
1715 if (this->autotile.workspace_blacklist) {
1716 return !this->autotile.workspaces.contains(workspace->m_id);
1717 } else {
1718 return this->autotile.workspaces.empty()
1719 || this->autotile.workspaces.contains(workspace->m_id);
1720 }
1721}