Parent directory

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}