Hy3Node.cpp
27651 bytes
1#include <cstdint>
2#include <sstream>
3#include <stdexcept>
4
5#include <bits/ranges_util.h>
6#include <hyprland/src/Compositor.hpp>
7#include <hyprland/src/desktop/state/FocusState.hpp>
8#include <hyprland/src/config/ConfigManager.hpp>
9#include <hyprland/src/defines.hpp>
10#include <hyprland/src/desktop/view/window/WindowPresentation.hpp>
11#include <hyprland/src/plugins/PluginAPI.hpp>
12#include <hyprland/src/config/shared/workspace/WorkspaceRuleManager.hpp>
13#include <hyprutils/math/Box.hpp>
14#include <hyprland/src/config/shared/complex/ComplexDataTypes.hpp>
15#include <hyprland/src/desktop/state/WindowState.hpp>
16
17#include "log.hpp"
18#include "Hy3Layout.hpp"
19#include "Hy3Node.hpp"
20#include "globals.hpp"
21
22using Desktop::View::CWindow;
23
24const float MIN_RATIO = 0.0f;
25
26Hy3GroupNode::Hy3GroupNode(Hy3GroupLayout layout): layout(layout) {
27 if (!isTab()) {
28 this->previous_nontab_layout = layout;
29 }
30}
31
32bool Hy3Node::is_root() { return is_group() && as_group().layout == Hy3GroupLayout::Root; }
33bool Hy3Node::is_root_group() { return !is_root() && parent->is_root(); }
34
35Hy3RootNode::Hy3RootNode(Hy3Layout* layout)
36 : Hy3GroupNode(Hy3GroupLayout::Root), algo(layout) {}
37
38Hy3RootNode* Hy3Node::root() {
39 auto* node = this;
40 while (!node->is_root() && node->parent.get() != nullptr) {
41 node = node->parent.get();
42 }
43 return dynamic_cast<Hy3RootNode*>(node);
44}
45
46Hy3Layout* Hy3Node::layout() {
47 auto* r = root();
48 return r ? r->algo : nullptr;
49}
50
51void Hy3Node::assertNotRoot() {
52 if (this->is_root()) {
53 hy3_log(ERR, "assertNotRoot failed: node {:x} is root", (uintptr_t) this);
54 throw std::runtime_error("operation called on root node");
55 }
56}
57
58bool Hy3GroupNode::hasChild(Hy3Node& node) {
59 for (auto& child: this->children) {
60 if (child.get() == &node) return true;
61
62 if (child->is_group()) {
63 if (child->as_group().hasChild(node)) return true;
64 }
65 }
66
67 return false;
68}
69
70auto Hy3GroupNode::findChild(Hy3Node& child) -> std::list<UP<Hy3Node>>::iterator {
71 for (auto it = children.begin(); it != children.end(); ++it) {
72 if (it->get() == &child) return it;
73 }
74 return children.end();
75}
76
77void Hy3GroupNode::insertChild(std::list<UP<Hy3Node>>::iterator pos, UP<Hy3Node> child) {
78 child->parent = this->self;
79 if (focused_child == nullptr) focused_child = child.get();
80 children.insert(pos, std::move(child));
81 if (ephemeral == Ephemeral::Staged && children.size() >= 2)
82 ephemeral = Ephemeral::Active;
83}
84
85void Hy3GroupNode::insertChild(UP<Hy3Node> child) {
86 insertChild(children.end(), std::move(child));
87}
88
89UP<Hy3Node> Hy3GroupNode::extractChildRaw(std::list<UP<Hy3Node>>::iterator it) {
90 auto* child_ptr = it->get();
91
92 // Fix focused_child if we're extracting it
93 if (focused_child == child_ptr) {
94 if (children.size() <= 1) {
95 focused_child = nullptr;
96 } else if (it == children.begin()) {
97 focused_child = std::next(it)->get();
98 } else {
99 focused_child = std::prev(it)->get();
100 }
101 }
102
103 auto up = std::move(*it);
104 children.erase(it);
105 up->parent.reset();
106 return up;
107}
108
109UP<Hy3Node> Hy3GroupNode::extractChildRaw(Hy3Node& child) {
110 auto it = findChild(child);
111 if (it == children.end()) return nullptr;
112 return extractChildRaw(it);
113}
114
115UP<Hy3Node> Hy3GroupNode::extractChild(Hy3Node& child) {
116 if (!child.is_root()) {
117 auto& actor = child.getExpandActor();
118 if (actor.is_group()) {
119 actor.as_group().collapseExpansions();
120 }
121 }
122
123 auto extracted = extractChildRaw(child);
124 if (!extracted) return nullptr;
125
126 group_focused = false;
127
128 if (!children.empty()) {
129 auto child_count = children.size();
130 auto splitmod = -((1.0 - extracted->size_ratio) / child_count);
131
132 for (auto& c: children) {
133 c->size_ratio += splitmod;
134 }
135 }
136
137 extracted->size_ratio = 1.0;
138 return extracted;
139}
140
141UP<Hy3Node> Hy3GroupNode::replaceChild(std::list<UP<Hy3Node>>::iterator it, UP<Hy3Node> replacement) {
142 replacement->parent = this->self;
143 replacement->size_ratio = (*it)->size_ratio;
144 if (focused_child == it->get()) focused_child = replacement.get();
145 auto old = std::exchange(*it, std::move(replacement));
146 old->size_ratio = 1.0;
147 old->parent.reset();
148 return old;
149}
150
151void Hy3GroupNode::collapseExpansions() {
152 if (this->expand_focused == ExpandFocusType::NotExpanded) return;
153 this->expand_focused = ExpandFocusType::NotExpanded;
154
155 Hy3Node* node = this->focused_child;
156
157 while (node->is_group() && node->as_group().expand_focused == ExpandFocusType::Stack) {
158 auto& group = node->as_group();
159 group.expand_focused = ExpandFocusType::NotExpanded;
160 node = group.focused_child;
161 }
162}
163
164void Hy3GroupNode::setLayout(Hy3GroupLayout layout) {
165 if (layout == Hy3GroupLayout::Root) return; // root layout is immutable
166 this->layout = layout;
167
168 if (!isTab()) {
169 this->previous_nontab_layout = layout;
170 }
171}
172
173void Hy3GroupNode::setEphemeral(GroupEphemeralityOption ephemeral) {
174 switch (ephemeral) {
175 case GroupEphemeralityOption::Standard: this->ephemeral = Ephemeral::Off; break;
176 case GroupEphemeralityOption::ForceEphemeral:
177 this->ephemeral = this->children.size() == 1 ? Ephemeral::Staged : Ephemeral::Active;
178 break;
179 case GroupEphemeralityOption::Ephemeral:
180 // no change
181 break;
182 }
183}
184
185bool Hy3Node::valid() const {
186 if (dynamic_cast<const Hy3GroupNode*>(this)) return true;
187 if (auto* t = dynamic_cast<const Hy3TargetNode*>(this)) return !t->target.expired();
188 return false;
189}
190
191Hy3NodeType Hy3Node::type() const {
192 if (dynamic_cast<const Hy3GroupNode*>(this)) return Hy3NodeType::Group;
193 if (dynamic_cast<const Hy3TargetNode*>(this)) return Hy3NodeType::Target;
194 throw std::runtime_error("Attempted to get Hy3NodeType of uninitialized Hy3Node data");
195}
196
197bool Hy3Node::is_group() const { return dynamic_cast<const Hy3GroupNode*>(this) != nullptr; }
198
199bool Hy3Node::is_target() const { return dynamic_cast<const Hy3TargetNode*>(this) != nullptr; }
200
201Hy3GroupNode& Hy3Node::as_group() {
202 auto* gn = dynamic_cast<Hy3GroupNode*>(this);
203 if (!gn) throw std::runtime_error("Attempted to get group value of a non-group Hy3Node");
204 return *gn;
205}
206
207SP<Layout::ITarget> Hy3Node::as_target() {
208 auto* tn = dynamic_cast<Hy3TargetNode*>(this);
209 if (!tn) throw std::runtime_error("Attempted to get target value of a non-target Hy3Node");
210 if (tn->target.expired()) throw std::runtime_error("Attempted to upgrade an expired Hy3Node target");
211 return tn->target.lock();
212}
213
214PHLWINDOW Hy3Node::as_window() {
215 return this->as_target()->window();
216}
217
218UP<Hy3Node> Hy3Node::create(SP<Layout::ITarget> target) {
219 auto up = makeUnique<Hy3TargetNode>();
220 up->target = target;
221 UP<Hy3Node> result = std::move(up);
222 result->self = WP<Hy3Node>(result);
223 return result;
224}
225
226UP<Hy3Node> Hy3Node::create(Hy3GroupLayout group_layout) {
227 auto up = makeUnique<Hy3GroupNode>(group_layout);
228 UP<Hy3Node> result = std::move(up);
229 result->self = WP<Hy3Node>(result);
230 return result;
231}
232
233bool Hy3Node::operator==(const Hy3Node& rhs) const { return this == &rhs; }
234
235void Hy3Node::focus(bool warp, Desktop::eFocusReason reason) {
236 this->markFocused();
237
238 g_pInputManager->unconstrainMouse();
239
240 switch (this->type()) {
241 case Hy3NodeType::Target: {
242 auto window = this->as_window();
243 window->setHidden(false);
244 Desktop::focusState()->fullWindowFocus(window, reason);
245 if (warp) {
246 const auto box = window->layoutBox();
247 Hy3Layout::warpCursorToBox(box.pos(), box.size());
248 }
249 break;
250 }
251 case Hy3NodeType::Group: {
252 Desktop::focusState()->resetWindowFocus();
253 for (auto& window: this->windows()) {
254 Desktop::windowState()->raise(window.m_self.lock());
255 }
256
257 if (warp) Hy3Layout::warpCursorToBox(this->visualBox.pos(), this->visualBox.size());
258 break;
259 }
260 }
261}
262
263void markGroupFocusedRecursive(Hy3GroupNode& group) {
264 group.group_focused = true;
265 for (auto& child: group.children) {
266 if (child->is_group()) markGroupFocusedRecursive(child->as_group());
267 }
268}
269
270void Hy3Node::markFocused() {
271 auto* root = this->root();
272
273 // update focus
274 if (this->is_group()) {
275 markGroupFocusedRecursive(this->as_group());
276 }
277
278 for (auto& ancestor: this->ancestors()) {
279 auto& group = ancestor.parent->as_group();
280 group.focused_child = &ancestor;
281 group.group_focused = false;
282 }
283
284 root->updateDecos();
285}
286
287Hy3Node& Hy3Node::getFocusedNode(bool ignore_group_focus, bool stop_at_expanded) {
288 switch (this->type()) {
289 case Hy3NodeType::Target: return *this;
290 case Hy3NodeType::Group: {
291 auto& group = this->as_group();
292
293 if (group.focused_child == nullptr || (!ignore_group_focus && group.group_focused)
294 || (stop_at_expanded && group.expand_focused != ExpandFocusType::NotExpanded))
295 {
296 return *this;
297 } else {
298 return group.focused_child->getFocusedNode(ignore_group_focus, stop_at_expanded);
299 }
300 }
301 }
302 throw std::runtime_error("getFocusedNode: invalid node type");
303}
304
305bool Hy3Node::isIndirectlyFocused() {
306 for (auto& node: this->ancestors()) {
307 auto& group = node.parent->as_group();
308 if (!group.group_focused && group.focused_child != &node) return false;
309 }
310
311 return true;
312}
313
314Hy3Node& Hy3Node::getExpandActor() {
315 for (auto& node: this->ancestors()) {
316 if (node.parent->as_group().expand_focused == ExpandFocusType::NotExpanded)
317 return node;
318 }
319 hy3_log(ERR, "getExpandActor: no non-expanded ancestor found for node {:x}", (uintptr_t) this);
320 return *this;
321}
322
323Hy3Node& Hy3Node::getPlacementActor() {
324 for (auto& node: this->getExpandActor().ancestors()) {
325 if (!node.parent->as_group().locked)
326 return node;
327 }
328 hy3_log(ERR, "getPlacementActor: no non-locked ancestor found for node {:x}", (uintptr_t) this);
329 return *this;
330}
331
332void Hy3Node::recalcSizePosRecursive(CBox offsets, bool no_animation) {
333 // clang-format off
334 static const auto p_gaps_in = CConfigValue<Config::IComplexConfigValue>("general:gaps_in");
335 static const auto tab_bar_height = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:height");
336 static const auto tab_bar_padding = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding");
337 static const auto group_inset = CConfigValue<Config::INTEGER>("plugin:hy3:group_inset");
338 // clang-format on
339
340 this->logicalBox = CBox(
341 this->visualBox.x - offsets.x, this->visualBox.y - offsets.y,
342 this->visualBox.w + offsets.x + offsets.w, this->visualBox.h + offsets.y + offsets.h
343 );
344
345 // Keep in sync with WindowTarget::updatePos
346 if (this->is_target()) {
347 this->as_window()->setHidden(this->hidden);
348 this->as_target()->setPositionGlobal({.logicalBox = this->logicalBox, .visualBox = this->visualBox});
349 // warp on hidden fixes bounding boxes for the tab click handler
350 if (no_animation || this->hidden) this->as_target()->warpPositionSize();
351 return;
352 }
353
354 auto tpos = this->visualBox.pos();
355 auto tsize = this->visualBox.size();
356
357 auto& group = this->as_group();
358 auto workspace_rule = Config::workspaceRuleMgr()->getWorkspaceRuleFor(this->layout()->workspace());
359 auto gaps_in = workspace_rule.and_then([](auto r) { return r.m_gapsIn; }).value_or(*sc<Config::CCssGapData*>(p_gaps_in.ptr()));
360
361 auto expand_focused = group.expand_focused != ExpandFocusType::NotExpanded;
362 bool directly_contains_expanded =
363 expand_focused
364 && (group.focused_child->is_target()
365 || group.focused_child->as_group().expand_focused == ExpandFocusType::NotExpanded);
366
367 auto child_count = group.children.size();
368
369 // Latch/expanded: expanded node covers full parent area with parent offsets
370 if (group.expand_focused == ExpandFocusType::Latch) {
371 auto* expanded_node = group.focused_child;
372
373 while (expanded_node != nullptr && expanded_node->is_group()
374 && expanded_node->as_group().expand_focused != ExpandFocusType::NotExpanded)
375 {
376 expanded_node = expanded_node->as_group().focused_child;
377 }
378
379 if (expanded_node == nullptr) {
380 hy3_log(
381 ERR,
382 "recalcSizePosRecursive: unable to find expansion target of latch node {:x}",
383 (uintptr_t) this
384 );
385 errorNotif();
386 return;
387 }
388
389 expanded_node->visualBox = CBox(tpos, tsize);
390 expanded_node->setHidden(this->hidden);
391
392 expanded_node->recalcSizePosRecursive(offsets, no_animation);
393 }
394
395 // Compute constraint for splits: total visible space minus inter-child gaps
396 double inter_gap = 0.0;
397 double constraint = 0.0;
398
399 switch (group.layout) {
400 case Hy3GroupLayout::SplitH:
401 inter_gap = gaps_in.m_left + gaps_in.m_right;
402 constraint = tsize.x - (child_count > 1 ? (child_count - 1) * inter_gap : 0);
403 break;
404 case Hy3GroupLayout::SplitV:
405 inter_gap = gaps_in.m_top + gaps_in.m_bottom;
406 constraint = tsize.y - (child_count > 1 ? (child_count - 1) * inter_gap : 0);
407 break;
408 case Hy3GroupLayout::Tabbed:
409 case Hy3GroupLayout::Root: break;
410 }
411
412 double ratio_mul =
413 group.isSplit() ? child_count <= 0 ? 0 : constraint / child_count : 0;
414
415 double offset = 0;
416
417 for (auto& child: group.children) {
418 bool is_first = (child.get() == group.children.front().get());
419 bool is_last = (child.get() == group.children.back().get());
420 int inset = is_first && is_last && !this->is_root_group() ? *group_inset : 0;
421
422 if (directly_contains_expanded && child.get() == group.focused_child) {
423 // Advance offset past this child's visible share
424 if (group.isSplit()) {
425 offset += child->size_ratio * ratio_mul - inset;
426 if (!is_last) offset += inter_gap;
427 }
428 continue;
429 }
430
431 CBox child_offsets;
432
433 switch (group.layout) {
434 case Hy3GroupLayout::SplitH: {
435 double child_w = child->size_ratio * ratio_mul;
436
437 child->visualBox = CBox(tpos.x + offset, tpos.y, child_w - inset, tsize.y);
438 child->hidden = this->hidden || expand_focused;
439
440 child_offsets.x = is_first ? offsets.x : gaps_in.m_left;
441 child_offsets.w = (is_last ? offsets.w : gaps_in.m_right) + inset;
442 child_offsets.y = offsets.y;
443 child_offsets.h = offsets.h;
444
445 offset += child_w;
446 if (!is_last) offset += inter_gap;
447
448 child->recalcSizePosRecursive(child_offsets, no_animation);
449 break;
450 }
451 case Hy3GroupLayout::SplitV: {
452 double child_h = child->size_ratio * ratio_mul;
453
454 child->visualBox = CBox(tpos.x, tpos.y + offset, tsize.x, child_h - inset);
455 child->hidden = this->hidden || expand_focused;
456
457 child_offsets.y = (is_first ? offsets.y : gaps_in.m_top) + inset;
458 child_offsets.h = is_last ? offsets.h : gaps_in.m_bottom;
459 child_offsets.x = offsets.x;
460 child_offsets.w = offsets.w;
461
462 offset += child_h;
463 if (!is_last) offset += inter_gap;
464
465 child->recalcSizePosRecursive(child_offsets, no_animation);
466 break;
467 }
468 case Hy3GroupLayout::Tabbed: {
469 double tab_offset = (double)*tab_bar_height + (double)*tab_bar_padding;
470
471 child->visualBox = CBox(tpos.x, tpos.y + tab_offset, tsize.x, tsize.y - tab_offset);
472 child->hidden = this->hidden || expand_focused || group.focused_child != child.get();
473
474 // Tab bar makes child non-edge on top
475 child_offsets.x = offsets.x;
476 child_offsets.y = offsets.y + tab_offset;
477 child_offsets.w = offsets.w;
478 child_offsets.h = offsets.h;
479
480 child->recalcSizePosRecursive(child_offsets, no_animation);
481 break;
482 }
483 case Hy3GroupLayout::Root: {
484 child->visualBox = CBox(tpos, tsize);
485 child->hidden = this->hidden;
486 child->recalcSizePosRecursive(offsets, no_animation);
487 break;
488 }
489 }
490 }
491
492 this->updateTabBar(no_animation);
493}
494
495// Find the visible window with the highest z-order in this subtree.
496static CWindow* findTopVisibleWindow(Hy3Node& node) {
497 CWindow* result = nullptr;
498 const auto& compositor_windows = Desktop::windowState()->windows();
499 auto it = compositor_windows.begin();
500 for (auto& window: node.windows(true)) {
501 for (auto search = it; search != compositor_windows.end(); ++search) {
502 if (search->get() == &window) {
503 result = &window;
504 it = search;
505 break;
506 }
507 }
508 }
509 return result;
510}
511
512void Hy3Node::updateTabBar(bool no_animation) {
513 if (this->type() == Hy3NodeType::Group) {
514 auto& group = this->as_group();
515
516 if (group.isTab()) {
517 if (!group.tab_bar) group.tab_bar = Hy3TabGroup::create(*this);
518 group.tab_bar->updateWithGroup(*this, no_animation);
519
520 auto top_window = findTopVisibleWindow(*this);
521 group.tab_bar->target_window = top_window ? top_window->m_self.lock() : nullptr;
522 if (top_window != nullptr) group.tab_bar->workspace = top_window->m_workspace;
523 } else if (group.tab_bar) {
524 group.tab_bar.release();
525 }
526 }
527}
528
529void Hy3Node::updateTabBarRecursive() {
530 for (auto& node: this->ancestors()) {
531 node.updateTabBar();
532 }
533}
534
535void Hy3Node::updateDecos() {
536 switch (this->type()) {
537 case Hy3NodeType::Target:
538 this->as_window()->presentation().refreshValues();
539 break;
540 case Hy3NodeType::Group:
541 for (auto& child: this->as_group().children) {
542 child->updateDecos();
543 }
544
545 this->updateTabBar();
546 }
547}
548
549std::string Hy3Node::getTitle() {
550 switch (this->type()) {
551 case Hy3NodeType::Target: return this->as_window()->metadata().title();
552 case Hy3NodeType::Group:
553 std::string title;
554 auto& group = this->as_group();
555
556 switch (group.layout) {
557 case Hy3GroupLayout::Root: title = "[R] "; break;
558 case Hy3GroupLayout::SplitH: title = "[H] "; break;
559 case Hy3GroupLayout::SplitV: title = "[V] "; break;
560 case Hy3GroupLayout::Tabbed: title = "[T] "; break;
561 }
562
563 if (group.focused_child == nullptr) {
564 title += "Group";
565 } else {
566 title += group.focused_child->getTitle();
567 }
568
569 return title;
570 }
571
572 return "";
573}
574
575bool Hy3Node::isUrgent() {
576 for (auto& window: this->windows()) {
577 if (window.m_hints & Desktop::View::WINDOW_HINT_URGENT) return true;
578 }
579 return false;
580}
581
582void Hy3Node::setHidden(bool hidden) {
583 this->hidden = hidden;
584
585 if (this->is_group()) {
586 for (auto& child: this->as_group().children) {
587 child->setHidden(hidden);
588 }
589 }
590}
591
592Hy3Node* Hy3Node::findNodeForTabGroup(Hy3TabGroup& tab_group) {
593 if (this->is_group()) {
594 if (this->hidden) return nullptr;
595 auto& group = this->as_group();
596
597 if (group.isTab() && group.tab_bar.get() == &tab_group) {
598 return this;
599 }
600
601 for (auto& node: group.children) {
602 auto* r = node->findNodeForTabGroup(tab_group);
603 if (r != nullptr) return r;
604 }
605 } else return nullptr;
606
607 return nullptr;
608}
609
610std::generator<Hy3Node&> Hy3Node::ancestors() {
611 auto* node = this;
612 while (!node->is_root()) {
613 co_yield *node;
614 node = node->parent.get();
615 }
616}
617
618std::generator<CWindow&> Hy3Node::windows(bool visibleOnly) {
619 if (this->is_target()) {
620 co_yield *this->as_window();
621 } else {
622 auto& group = this->as_group();
623 if (visibleOnly
624 && (group.isTab()
625 || group.expand_focused != ExpandFocusType::NotExpanded))
626 {
627 if (group.focused_child != nullptr) {
628 for (auto& window: group.focused_child->windows(true)) {
629 co_yield window;
630 }
631 }
632 } else {
633 for (auto& child: group.children) {
634 for (auto& window: child->windows(visibleOnly)) {
635 co_yield window;
636 }
637 }
638 }
639 }
640}
641
642
643std::string Hy3Node::debugNode() {
644 std::stringstream buf;
645 std::string addr = "0x" + std::to_string((size_t) this);
646 switch (this->type()) {
647 case Hy3NodeType::Target:
648 buf << "window(" << this << " of " << this->parent.get() << ") [hypr " << this->as_window().get() << "] size ratio: " << this->size_ratio;
649 break;
650 case Hy3NodeType::Group:
651 buf << "group(" << this << " of " << this->parent.get() << ") [";
652
653 auto& group = this->as_group();
654 switch (group.layout) {
655 case Hy3GroupLayout::Root: {
656 auto* l = this->layout();
657 auto ws = l ? l->workspace() : nullptr;
658 buf << "root " << (ws ? ws->m_id : -1);
659 break;
660 }
661 case Hy3GroupLayout::SplitH: buf << "splith"; break;
662 case Hy3GroupLayout::SplitV: buf << "splitv"; break;
663 case Hy3GroupLayout::Tabbed: buf << "tabs"; break;
664 }
665
666 buf << "] size ratio: ";
667 buf << this->size_ratio;
668
669 if (group.expand_focused != ExpandFocusType::NotExpanded) {
670 buf << ", has-expanded";
671 }
672
673 if (group.ephemeral != Ephemeral::Off) {
674 buf << ", ephemeral" << (group.ephemeral == Ephemeral::Staged ? "(staged)" : "");
675 }
676
677 if (group.containment) {
678 buf << ", containment";
679 }
680
681 for (auto& child: group.children) {
682 buf << "\n|-";
683 if (!child) {
684 buf << "nullptr";
685 } else {
686 // this is terrible
687 for (char c: child->debugNode()) {
688 buf << c;
689 if (c == '\n') buf << " ";
690 }
691 }
692 }
693
694 break;
695 }
696
697 return buf.str();
698}
699
700static bool shouldCollapseNode(Hy3Node* node, CollapsePolicy policy) {
701 if (node->is_root()) return false;
702 auto& group = node->as_group();
703 if (group.children.size() != 1) return false;
704 auto* child = group.children.front().get();
705 if (node->is_root_group() && !child->is_group()) return false;
706 if (policy == CollapsePolicy::SingleNodeGroups || group.ephemeral == Ephemeral::Active) return true;
707
708 if (policy == CollapsePolicy::EmptySplits && group.isSplit()) return true;
709
710 if (child->is_group()) {
711 auto& cgroup = child->as_group();
712 if (group.isSplit() && cgroup.isSplit()) return true;
713 if (cgroup.children.size() == 1 && group.isTab() && cgroup.isTab()) return true;
714 }
715
716 return false;
717}
718
719static void collapseSingleParentInternal(Hy3Node* into) {
720 auto* parent = into->parent.get();
721 auto& parentGroup = parent->as_group();
722 auto it = parentGroup.findChild(*into);
723 auto& intoGroup = into->as_group();
724
725 hy3_log(
726 TRACE,
727 "collapsing {:x} in favor of {:x}",
728 (uintptr_t) into,
729 (uintptr_t) intoGroup.children.front().get()
730 );
731
732 auto childUp = intoGroup.extractChildRaw(intoGroup.children.begin());
733 auto* child = childUp.get();
734 auto old = parentGroup.replaceChild(it, std::move(childUp));
735
736 // HACK: steal titlebar from parent if we have a new node, prevents visual issues if rewrapped
737 if (child->is_group() && old->as_group().isTab() && child->as_group().isTab()) {
738 auto& n = child->as_group().tab_bar;
739 auto& o = old->as_group().tab_bar;
740 if (n->bar.entries.empty() || n->bar.entries.front().vertical_pos->value() == 1) n = std::move(o);
741 }
742}
743
744Hy3Node* Hy3Node::collapseParents(CollapsePolicy policy) {
745 if (this->is_root()) return this;
746
747 if (!this->is_group()) {
748 this->parent->collapseParents(CollapsePolicy::InvalidOnly);
749 return this;
750 }
751
752 auto& group = this->as_group();
753
754 if (group.children.empty()) {
755 auto* p = this->parent.get();
756 Hy3Node* merged = nullptr;
757 p->extractAndMerge(*this, &merged, CollapsePolicy::InvalidOnly);
758 return merged;
759 }
760
761 hy3_log(LOG, "ShouldCollapse {:x} policy {}: {}", (uintptr_t)this, (int)policy, shouldCollapseNode(this, policy));
762 if (shouldCollapseNode(this, policy)) {
763 auto* parent_node = this->parent.get();
764 collapseSingleParentInternal(this);
765 return parent_node->collapseParents(CollapsePolicy::InvalidOnly);
766 } else {
767 this->parent->collapseParents(CollapsePolicy::InvalidOnly);
768 }
769
770 return this;
771}
772
773UP<Hy3Node> Hy3Node::extractAndMerge(
774 Hy3Node& child,
775 Hy3Node** out_parent,
776 CollapsePolicy policy
777) {
778 hy3_log(
779 TRACE,
780 "extractAndMerge: extracting {:x} from {:x}",
781 (uintptr_t) &child,
782 (uintptr_t) this
783 );
784
785 auto& group = this->as_group();
786 auto extracted = group.extractChild(child);
787 if (!extracted) {
788 hy3_log(
789 ERR,
790 "unable to extract child node {:x} from parent node {:x}",
791 (uintptr_t) &child,
792 (uintptr_t) this
793 );
794 errorNotif();
795 return nullptr;
796 }
797
798 auto* merged = this->collapseParents(policy);
799 if (out_parent != nullptr) *out_parent = merged;
800
801 return extracted;
802}
803
804void Hy3Node::insertAndMerge(
805 std::list<UP<Hy3Node>>::iterator pos,
806 UP<Hy3Node> child,
807 CollapsePolicy policy
808) {
809 this->as_group().insertChild(pos, std::move(child));
810 this->collapseParents(policy);
811}
812
813void Hy3Node::insertAndMerge(UP<Hy3Node> child, CollapsePolicy policy) {
814 this->as_group().insertChild(std::move(child));
815 this->collapseParents(policy);
816}
817
818void Hy3Node::wrap(Hy3GroupLayout layout, GroupEphemeralityOption ephemeral, bool change) {
819 auto& parentGroup = this->parent->as_group();
820 if (change && !this->parent->is_root() && parentGroup.children.size() == 1) {
821 parentGroup.setLayout(layout);
822 parentGroup.setEphemeral(ephemeral);
823 this->layout()->recalcGeometry();
824 this->parent->updateTabBarRecursive();
825 return;
826 }
827
828 auto it = parentGroup.findChild(*this);
829
830 auto group_up = Hy3Node::create(layout);
831 auto& group_node = *group_up;
832
833 auto this_up = parentGroup.replaceChild(it, std::move(group_up));
834
835 auto& group = group_node.as_group();
836 group.insertChild(std::move(this_up));
837 group.group_focused = false;
838 group.focused_child = this;
839 if (ephemeral == GroupEphemeralityOption::Ephemeral
840 || ephemeral == GroupEphemeralityOption::ForceEphemeral)
841 group.setEphemeral(GroupEphemeralityOption::ForceEphemeral);
842
843 this->layout()->recalcGeometry();
844 group_node.updateTabBarRecursive();
845}
846
847
848Hy3Node* getOuterChild(Hy3GroupNode& group, ShiftDirection direction) {
849 switch (direction) {
850 case ShiftDirection::Left:
851 case ShiftDirection::Up: return group.children.front().get(); break;
852 case ShiftDirection::Right:
853 case ShiftDirection::Down: return group.children.back().get(); break;
854 default: throw std::runtime_error("invalid ShiftDirection");
855 }
856}
857
858Hy3Node* Hy3Node::getImmediateSibling(ShiftDirection direction) {
859 auto& group = this->parent->as_group();
860
861 auto iter = group.findChild(*this);
862 if (iter == group.children.end()) return nullptr;
863
864 switch (direction) {
865 case ShiftDirection::Left:
866 case ShiftDirection::Up:
867 if (iter == group.children.begin()) return nullptr;
868 return std::prev(iter)->get();
869 case ShiftDirection::Right:
870 case ShiftDirection::Down: {
871 auto next = std::next(iter);
872 if (next == group.children.end()) return nullptr;
873 return next->get();
874 }
875 default: throw std::runtime_error("invalid ShiftDirection");
876 }
877}
878
879
880Axis getAxis(Hy3GroupLayout layout) {
881 switch (layout) {
882 case Hy3GroupLayout::SplitH: return Axis::Horizontal;
883 case Hy3GroupLayout::SplitV: return Axis::Vertical;
884 default: return Axis::None;
885 }
886}
887
888Axis getAxis(ShiftDirection direction) {
889 switch (direction) {
890 case ShiftDirection::Left:
891 case ShiftDirection::Right: return Axis::Horizontal;
892 case ShiftDirection::Down:
893 case ShiftDirection::Up: return Axis::Vertical;
894 default: return Axis::None;
895 }
896}
897
898Hy3Node* Hy3Node::findNeighbor(ShiftDirection direction) {
899 for (auto& node: this->ancestors()) {
900 auto& parent_group = node.parent->as_group();
901
902 if (parent_group.isSplit()
903 && getAxis(parent_group.layout) == getAxis(direction)
904 && getOuterChild(parent_group, direction) != &node)
905 {
906 return node.getImmediateSibling(direction);
907 }
908 }
909
910 return nullptr;
911}
912
913int directionToIteratorIncrement(ShiftDirection direction) {
914 switch (direction) {
915 case ShiftDirection::Left:
916 case ShiftDirection::Up: return -1;
917 case ShiftDirection::Right:
918 case ShiftDirection::Down: return 1;
919 default: throw std::runtime_error("Unknown ShiftDirection");
920 }
921}
922
923void Hy3Node::resize(ShiftDirection direction, double delta, bool no_animation) {
924 auto* parent_node = this->parent.get();
925 auto& containing_group = parent_node->as_group();
926
927 if (containing_group.isSplit()
928 && getAxis(direction) == getAxis(containing_group.layout))
929 {
930 double parent_size =
931 getAxis(direction) == Axis::Horizontal ? parent_node->visualBox.w : parent_node->visualBox.h;
932 auto ratio_mod = delta * (float) containing_group.children.size() / parent_size;
933
934 const auto end_of_children = containing_group.children.end();
935 auto iter = containing_group.findChild(*this);
936
937 if (iter != end_of_children) {
938 const auto outermost_node_in_group = getOuterChild(containing_group, direction);
939 if (this != outermost_node_in_group) {
940 auto inc = directionToIteratorIncrement(direction);
941 iter = std::next(iter, inc);
942 ratio_mod *= inc;
943 }
944
945 if (iter != end_of_children) {
946 auto* neighbor = iter->get();
947 auto requested_size_ratio = this->size_ratio + ratio_mod;
948 auto requested_neighbor_size_ratio = neighbor->size_ratio - ratio_mod;
949
950 if (requested_size_ratio >= MIN_RATIO && requested_neighbor_size_ratio >= MIN_RATIO) {
951 this->size_ratio = requested_size_ratio;
952 neighbor->size_ratio = requested_neighbor_size_ratio;
953
954 this->layout()->recalcGeometry(no_animation);
955 }
956 }
957 }
958 }
959}