Parent directory

TabGroup.cpp

31639 bytes
  1#include "TabGroup.hpp"
  2#include <optional>
  3#include <utility>
  4#include <vector>
  5
  6#include <GLES2/gl2.h>
  7#include <cairo/cairo.h>
  8#include <hyprgraphics/color/Color.hpp>
  9#include <hyprland/src/Compositor.hpp>
 10#include <hyprland/src/desktop/state/FocusState.hpp>
 11#include <hyprland/src/config/ConfigManager.hpp>
 12#include <hyprland/src/desktop/DesktopTypes.hpp>
 13#include <hyprland/src/workspace/HLWorkspace.hpp>
 14#include <hyprland/src/desktop/view/window/WindowPresentation.hpp>
 15#include <hyprland/src/desktop/view/types/Geometric.hpp>
 16#include <hyprland/src/desktop/view/window/WindowPresentation.hpp>
 17#include <hyprland/src/desktop/view/types/Geometric.hpp>
 18#include <hyprland/src/helpers/Color.hpp>
 19#include <hyprland/src/animation/AnimationManager.hpp>
 20#include <hyprland/src/managers/input/InputManager.hpp>
 21#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
 22#include <hyprland/src/render/OpenGL.hpp>
 23#include <hyprland/src/render/Texture.hpp>
 24#include <hyprland/src/config/shared/animation/AnimationTree.hpp>
 25#include <hyprutils/math/Box.hpp>
 26#include <hyprutils/math/Region.hpp>
 27#include <hyprutils/memory/SharedPtr.hpp>
 28#include <pango/pangocairo.h>
 29#include <pixman.h>
 30
 31#include "log.hpp"
 32#include "globals.hpp"
 33#include "render.hpp"
 34#include "render/Context.hpp"
 35#include "render/Renderer.hpp"
 36#include "render/pass/PassElement.hpp"
 37
 38using Hyprgraphics::CColor;
 39using Desktop::View::IGeometric;
 40
 41// This is a workaround CHyprColor not having working arithmetic operator...
 42template <typename... Args>
 43CHyprColor merge_colors(Args... colors) {
 44	auto merge_oklab = []<typename... Args2>(Args2... colors) {
 45		return CColor::SOkLab {
 46		    .l = ((colors.first * colors.second.l) + ...),
 47		    .a = ((colors.first * colors.second.a) + ...),
 48		    .b = ((colors.first * colors.second.b) + ...),
 49		};
 50	};
 51
 52	auto oklab = merge_oklab(std::make_pair(colors.first, colors.second.asOkLab())...);
 53	auto alpha = ((colors.first * colors.second.a) + ...);
 54
 55	// the alpha is linear, otherwise use the fact that CColor can take an OkLab and do the correct
 56	// conversion to an rgb
 57	return CHyprColor(Hyprgraphics::CColor(oklab), alpha);
 58}
 59
 60constexpr std::pair<double, double>
 61tab_entry_geometry(double group_width, double padding, bool outer, double offset, double width) {
 62	if (outer) return {offset * group_width + padding * 0.5, width * group_width - padding};
 63	return {offset * (group_width + padding), width * (group_width + padding) - padding};
 64}
 65
 66static_assert(tab_entry_geometry(100, 20, false, 0.0, 0.25) == std::pair {0.0, 10.0});
 67static_assert(tab_entry_geometry(100, 20, false, 0.75, 0.25) == std::pair {90.0, 10.0});
 68
 69Hy3TabBarEntry::Hy3TabBarEntry(Hy3TabBar& tab_bar, Hy3Node& node): tab_bar(tab_bar), node(&node) {
 70	auto& mgr = Animation::mgr();
 71	mgr->createAnimation(
 72	    0.0F,
 73	    this->active,
 74	    Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"),
 75	    AVARDAMAGE_NONE
 76	);
 77
 78	mgr->createAnimation(
 79	    0.0F,
 80	    this->focused,
 81	    Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"),
 82	    AVARDAMAGE_NONE
 83	);
 84
 85	mgr->createAnimation(
 86	    0.0F,
 87	    this->urgent,
 88	    Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"),
 89	    AVARDAMAGE_NONE
 90	);
 91
 92	mgr->createAnimation(
 93	    0.0F,
 94	    this->active_monitor,
 95	    Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"),
 96	    AVARDAMAGE_NONE
 97	);
 98
 99	mgr->createAnimation(
100	    -1.0F,
101	    this->offset,
102	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
103	    AVARDAMAGE_NONE
104	);
105
106	mgr->createAnimation(
107	    -1.0F,
108	    this->width,
109	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
110	    AVARDAMAGE_NONE
111	);
112
113	mgr->createAnimation(
114	    1.0F,
115	    this->vertical_pos,
116	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
117	    AVARDAMAGE_NONE
118	);
119
120	mgr->createAnimation(
121	    0.0F,
122	    this->fade_opacity,
123	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
124	    AVARDAMAGE_NONE
125	);
126
127	auto update_callback = [this](auto) { this->tab_bar.dirty = true; };
128
129	this->active->setUpdateCallback(update_callback);
130	this->focused->setUpdateCallback(update_callback);
131	this->urgent->setUpdateCallback(update_callback);
132	this->active_monitor->setUpdateCallback(update_callback);
133	this->focused->setUpdateCallback(update_callback);
134	this->offset->setUpdateCallback(update_callback);
135	this->width->setUpdateCallback(update_callback);
136	this->vertical_pos->setUpdateCallback(update_callback);
137	this->fade_opacity->setUpdateCallback(update_callback);
138
139	this->window_title = node.getTitle();
140	*this->urgent = node.isUrgent();
141
142	*this->vertical_pos = 0.0;
143	*this->fade_opacity = 1.0;
144}
145
146bool Hy3TabBarEntry::operator==(const Hy3Node& node) const { return this->node == &node; }
147
148bool Hy3TabBarEntry::operator==(const Hy3TabBarEntry& entry) const {
149	return this->node == entry.node;
150}
151
152void Hy3TabBarEntry::setActive(bool active) {
153	if (this->active->goal() != active) {
154		*this->active = active;
155	}
156}
157
158void Hy3TabBarEntry::setFocused(bool focused) {
159	if (this->focused->goal() != focused) {
160		*this->focused = focused;
161	}
162}
163
164void Hy3TabBarEntry::setUrgent(bool urgent) {
165	if (urgent && this->focused->goal() == 1.0) urgent = false;
166	if (this->urgent->goal() != urgent) {
167		*this->urgent = urgent;
168	}
169}
170
171void Hy3TabBarEntry::setWindowTitle(std::string title) {
172	if (this->window_title != title) {
173		this->window_title = title;
174		this->tab_bar.dirty = true;
175	}
176}
177
178void Hy3TabBarEntry::setMonitorActive(bool monitorActive) {
179	if (this->active_monitor->goal() != monitorActive) {
180		*this->active_monitor = monitorActive;
181	}
182}
183
184void Hy3TabBarEntry::beginDestroy() {
185	this->destroying = true;
186	*this->vertical_pos = 1.0;
187	*this->fade_opacity = 0.0;
188}
189
190void Hy3TabBarEntry::unDestroy() {
191	this->destroying = false;
192	*this->vertical_pos = 0.0;
193	*this->fade_opacity = 1.0;
194}
195
196bool Hy3TabBarEntry::shouldRemove() {
197	return this->destroying && (this->vertical_pos->value() == 1.0 || this->width->value() == 0.0);
198}
199
200void Hy3TabBarEntry::render(Render::CRenderContext& ctx, float scale, CBox& box, float opacity_mul) {
201	auto opacity = opacity_mul * this->fade_opacity->value();
202
203	// clang-format off
204	static const auto s_radius = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:radius");
205	static const auto border_width = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:border_width");
206	static const auto s_opacity = CConfigValue<Config::FLOAT>("plugin:hy3:tabs:opacity");
207	static const auto blur = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:blur");
208	static const auto col_active = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active");
209	static const auto col_border_active = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active_border");
210	static const auto col_focused = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:focused");
211	static const auto col_border_focused = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:focused_border");
212	static const auto col_urgent = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:urgent");
213	static const auto col_border_urgent = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:urgent_border");
214	static const auto col_locked = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:locked");
215	static const auto col_border_locked = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:locked_border");
216	static const auto col_active_alt_monitor = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active_alt_monitor");
217	static const auto col_border_active_alt_monitor = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active_alt_monitor_border");
218	static const auto col_inactive = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:inactive");
219	static const auto col_border_inactive = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:inactive_border");
220	// clang-format on
221
222	auto radius = std::min((double) *s_radius * scale, std::min(box.width * 0.5, box.height * 0.5));
223
224	auto color =
225	    this->mergeColors(*col_active, *col_focused, *col_urgent, *col_locked, *col_active_alt_monitor, *col_inactive);
226
227	auto border_color = this->mergeColors(
228	    *col_border_active,
229	    *col_border_focused,
230	    *col_border_urgent,
231	    *col_border_locked,
232	    *col_border_active_alt_monitor,
233	    *col_border_inactive
234	);
235
236	box.round();
237
238	// sometimes enabled before our renderer is called
239	Render::GL::g_pHyprOpenGL->scissor(ctx, nullptr);
240
241	Hy3Render::renderTab(
242	    ctx,
243	    box,
244	    opacity * *s_opacity,
245	    *blur,
246	    color,
247	    border_color,
248	    *border_width,
249	    radius
250	);
251
252	this->renderText(ctx, scale, box, opacity);
253}
254
255void Hy3TabBarEntry::renderText(Render::CRenderContext& ctx, float scale, CBox& box, float opacity) {
256	// clang-format off
257	static const auto render_text = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:render_text");
258	static const auto text_center = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:text_center");
259	static const auto text_font = CConfigValue<Config::STRING>("plugin:hy3:tabs:text_font");
260	static const auto text_height = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:text_height");
261	static const auto text_padding = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:text_padding");
262	static const auto col_text_active = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active_text");
263	static const auto col_text_focused = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:focused_text");
264	static const auto col_text_urgent = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:urgent_text");
265	static const auto col_text_locked = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:locked_text");
266	static const auto col_text_active_alt_monitor = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active_alt_monitor_text");
267	static const auto col_text_inactive = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:inactive_text");
268	// clang-format on
269
270	if (!*render_text) {
271		if (this->texture) this->texture.reset();
272		return;
273	}
274
275	auto padding = *text_padding * scale;
276	auto width = box.width - padding * 2;
277
278	if (!this->texture
279	    // clang-format off
280	    || this->last_render.window_title != this->window_title
281			|| this->last_render.text_font != *text_font
282	    || this->last_render.font_height != *text_height
283			|| this->last_render.scale != scale
284	    // clang-format on
285	    // If render width was smaller than full render width and size changed,
286	    // the text is probably ellipsized and needs to be recalculated.
287	    || (width != this->last_render.render_width
288	        && (width < this->last_render.full_logical_width
289	            || this->last_render.logical_width != this->last_render.full_logical_width)))
290	{
291		this->last_render.window_title = this->window_title;
292		this->last_render.text_font = *text_font;
293		this->last_render.font_height = *text_height;
294		this->last_render.scale = scale;
295		this->last_render.render_width = width;
296
297		auto* font_map = pango_cairo_font_map_get_default();
298		auto* context = pango_font_map_create_context(font_map);
299		auto* layout = pango_layout_new(context);
300		pango_layout_set_text(layout, this->window_title.c_str(), -1);
301
302		auto* font_desc = pango_font_description_from_string((*text_font).c_str());
303		pango_font_description_set_size(font_desc, *text_height * scale * PANGO_SCALE);
304		pango_layout_set_font_description(layout, font_desc);
305		pango_font_description_free(font_desc);
306
307		PangoRectangle ink_extents;
308		PangoRectangle logical_extents;
309
310		pango_layout_get_extents(layout, &ink_extents, &logical_extents);
311		this->last_render.full_logical_width = PANGO_PIXELS(logical_extents.width);
312
313		pango_layout_set_width(layout, width * PANGO_SCALE);
314		pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
315
316		pango_layout_get_extents(layout, &ink_extents, &logical_extents);
317
318		auto ink_x = PANGO_PIXELS(ink_extents.x);
319		auto ink_y = PANGO_PIXELS(ink_extents.y);
320		auto ink_width = PANGO_PIXELS(ink_extents.width);
321		auto ink_height = PANGO_PIXELS(ink_extents.height);
322
323		this->last_render.logical_width = PANGO_PIXELS(logical_extents.width);
324		this->last_render.logical_height = PANGO_PIXELS(logical_extents.height);
325
326		this->last_render.texture_x_offset = ink_x;
327		this->last_render.texture_y_offset = ink_y;
328		this->last_render.texture_width = ink_width;
329		this->last_render.texture_height = ink_height;
330
331		auto cairo_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ink_width, ink_height);
332		auto cairo = cairo_create(cairo_surface);
333
334		cairo_save(cairo);
335		cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR);
336		cairo_paint(cairo);
337		cairo_restore(cairo);
338
339		cairo_set_source_rgba(cairo, 1, 1, 1, 1);
340		cairo_move_to(cairo, -ink_x, -ink_y);
341
342		pango_cairo_update_layout(cairo, layout);
343		pango_cairo_show_layout(cairo, layout);
344
345		cairo_surface_flush(cairo_surface);
346
347		g_object_unref(layout);
348		g_object_unref(context);
349
350		this->texture = g_pHyprRenderer->createTexture(cairo_surface);
351
352		cairo_destroy(cairo);
353		cairo_surface_destroy(cairo_surface);
354	}
355
356	auto x_offset =
357	    *text_center ? box.w * 0.5 - this->last_render.logical_width * 0.5 : *text_padding;
358
359	auto y_offset = box.h * 0.5 - this->last_render.logical_height * 0.5;
360
361	auto texture_box = CBox {
362	    box.x + x_offset + this->last_render.texture_x_offset,
363	    box.y + y_offset + this->last_render.texture_y_offset,
364	    this->last_render.texture_width,
365	    this->last_render.texture_height,
366	};
367
368	texture_box.round();
369
370	auto c = mergeColors(
371	    *col_text_active,
372	    *col_text_focused,
373	    *col_text_urgent,
374	    *col_text_locked,
375	    *col_text_active_alt_monitor,
376	    *col_text_inactive
377	);
378
379	glBlendFunc(GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_ALPHA);
380	glBlendColor(c.r, c.g, c.b, c.a);
381
382	Render::GL::g_pHyprOpenGL->renderTexture(ctx, this->texture, texture_box, { .a = opacity });
383
384	glBlendColor(1, 1, 1, 1);
385	glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
386}
387
388CHyprColor Hy3TabBarEntry::mergeColors(
389    const CHyprColor& active,
390    const CHyprColor& focused,
391    const CHyprColor& urgent,
392    const CHyprColor& locked,
393    const CHyprColor& active_alt_monitor,
394    const CHyprColor& inactive
395) {
396	auto active_v = this->active->value();
397	auto urgent_v = std::max(0.0f, this->urgent->value() - active_v);
398	auto focused_v = std::max(0.0f, this->focused->value() - active_v - urgent_v);
399	auto locked_v = std::max(0.0f, this->tab_bar.locked->value() - active_v - urgent_v - focused_v);
400	auto inactive_v = 1.0f - (active_v + urgent_v + focused_v + locked_v);
401
402	auto active_monitor_v = this->active_monitor->value();
403	auto active_alt_monitor_v = active_v * (1.0 - active_monitor_v);
404	active_v *= active_monitor_v;
405
406	return merge_colors(
407	    std::make_pair(active_v, active),
408	    std::make_pair(urgent_v, urgent),
409	    std::make_pair(focused_v, focused),
410	    std::make_pair(locked_v, locked),
411	    std::make_pair(active_alt_monitor_v, active_alt_monitor),
412	    std::make_pair(inactive_v, inactive)
413	);
414}
415
416Hy3TabBar::Hy3TabBar() {
417	auto& mgr = Animation::mgr();
418	mgr->createAnimation(
419	    1.0f,
420	    this->fade_opacity,
421	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
422	    AVARDAMAGE_NONE
423	);
424
425	mgr->createAnimation(
426	    0.0F,
427	    this->locked,
428	    Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"),
429	    AVARDAMAGE_NONE
430	);
431
432	this->fade_opacity->setUpdateCallback([this](auto) { this->dirty = true; });
433	this->locked->setUpdateCallback([this](auto) { this->dirty = true; });
434}
435
436void Hy3TabBar::beginDestroy() {
437	for (auto& entry: this->entries) {
438		entry.beginDestroy();
439	}
440}
441
442void Hy3TabBar::tick() {
443	auto iter = this->entries.begin();
444
445	while (iter != this->entries.end()) {
446		if (iter->shouldRemove()) {
447			iter = this->entries.erase(iter);
448		} else {
449			if (iter->active_monitor->isBeingAnimated()) {
450				this->dirty = true;
451			}
452
453			iter = std::next(iter);
454		}
455	}
456
457	if (this->entries.empty()) this->destroy = true;
458}
459
460void Hy3TabBar::updateNodeList(std::list<UP<Hy3Node>>& nodes) {
461	std::list<Hy3TabBarEntry> pool;
462	pool.splice(pool.begin(), this->entries);
463
464	for (auto node = nodes.begin(); node != nodes.end(); ++node) {
465		auto match = std::find_if(pool.begin(), pool.end(), [&node](auto& entry) {
466			return **node == entry;
467		});
468
469		if (match != pool.end()) {
470			match->node = node->get();
471			this->entries.splice(this->entries.end(), pool, match);
472		}
473	}
474
475	// TODO: index match is wrong if a move results in the addition of a node and destruction of another in one op
476	auto entry = this->entries.begin();
477	int node_index = 0;
478	for (auto node = nodes.begin(); node != nodes.end(); ++node, ++node_index) {
479		if (entry != this->entries.end() && entry->node == node->get()) {
480			++entry;
481			continue;
482		}
483
484		auto match = std::find_if(pool.begin(), pool.end(), [&node_index](auto& entry) {
485			return entry.lastIndex == node_index;
486		});
487
488		if (match != pool.end()) {
489			match->node = node->get();
490			this->entries.splice(entry, pool, match);
491		}
492	}
493
494	entry = this->entries.begin();
495	node_index = 0;
496	for (auto node = nodes.begin(); node != nodes.end(); ++node, ++node_index) {
497		if (entry == this->entries.end() || entry->node != node->get()) {
498			entry = this->entries.emplace(entry, *this, **node);
499		}
500
501		entry->unDestroy();
502		entry->lastIndex = node_index;
503
504		auto* parent = (*node)->parent.get();
505		auto& parent_group = parent->as_group();
506		auto parent_focused = parent->isIndirectlyFocused();
507
508		entry->setFocused(
509		    parent_group.focused_child == node->get() || (parent_group.group_focused && parent_focused)
510		);
511
512		auto active = parent_focused && (parent_group.focused_child == node->get() || parent_group.group_focused);
513		entry->setActive(active);
514
515		auto last_monitor = Desktop::focusState()->monitor();
516		entry->setMonitorActive(active && (!last_monitor || (*node)->layout()->monitor() == last_monitor));
517
518		entry->setUrgent((*node)->isUrgent());
519		entry->setWindowTitle((*node)->getTitle());
520
521		entry = std::next(entry);
522	}
523
524	while (!pool.empty()) {
525		auto it = pool.begin();
526		if (!it->destroying) it->beginDestroy();
527		if (it->shouldRemove()) {
528			pool.erase(it);
529		} else {
530			this->entries.splice(this->entries.end(), pool, it);
531		}
532	}
533}
534
535void Hy3TabBar::updateAnimations(bool warp) {
536	int active_entries = 0;
537	for (auto& entry: this->entries) {
538		if (!entry.destroying) active_entries++;
539	}
540
541	float entry_width = active_entries == 0 ? 0.0 : 1.0 / active_entries;
542	float offset = 0.0;
543
544	auto entry = this->entries.begin();
545	while (entry != this->entries.end()) {
546		if (warp) {
547			if (entry->width->goal() == 0.0) {
548				// this->entries.erase(entry++);
549				entry = std::next(entry);
550				continue;
551			}
552
553			entry->offset->setValueAndWarp(offset);
554			entry->width->setValueAndWarp(entry_width);
555		} else {
556			auto warp_init = entry->offset->goal() == -1.0;
557
558			if (warp_init) {
559				entry->offset->setValueAndWarp(offset);
560				entry->width->setValueAndWarp(entry->vertical_pos->value() == 0.0 ? 0.0 : entry_width);
561			}
562
563			if (!entry->destroying) {
564				if (entry->offset->goal() != offset) *entry->offset = offset;
565				if ((warp_init || entry->width->goal() != 0.0) && entry->width->goal() != entry_width)
566					*entry->width = entry_width;
567			}
568		}
569
570		if (!entry->destroying) offset += entry->width->goal();
571		entry = std::next(entry);
572	}
573}
574
575void Hy3TabBar::setSize(Vector2D size) {
576	if (size == this->size) return;
577	this->size = size;
578}
579
580UP<Hy3TabGroup> Hy3TabGroup::create(Hy3Node& node) {
581	auto up = makeUnique<Hy3TabGroup>(node);
582	up->self = WP<Hy3TabGroup>(up);
583	g_tabGroups.push_back(up->self);
584	return up;
585}
586
587Hy3TabGroupWrapper::Hy3TabGroupWrapper() = default;
588Hy3TabGroupWrapper::Hy3TabGroupWrapper(UP<Hy3TabGroup> tg): inner(std::move(tg)) {}
589Hy3TabGroupWrapper::Hy3TabGroupWrapper(Hy3TabGroupWrapper&&) = default;
590Hy3TabGroupWrapper& Hy3TabGroupWrapper::operator=(Hy3TabGroupWrapper&&) = default;
591Hy3TabGroupWrapper::~Hy3TabGroupWrapper() { release(); }
592
593void Hy3TabGroupWrapper::release() {
594	if (inner.get()) {
595		inner->bar.beginDestroy();
596		g_destroyingTabGroups.push_back(std::move(inner));
597	}
598}
599
600Hy3TabGroupWrapper& Hy3TabGroupWrapper::operator=(UP<Hy3TabGroup> tg) {
601	release();
602	inner = std::move(tg);
603	return *this;
604}
605
606Hy3TabGroup::Hy3TabGroup(Hy3Node& node) {
607	auto& mgr = Animation::mgr();
608
609	mgr->createAnimation(
610	    Vector2D(0, 0),
611	    this->pos,
612	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
613	    AVARDAMAGE_NONE
614	);
615
616	mgr->createAnimation(
617	    Vector2D(0, 0),
618	    this->size,
619	    Config::animationTree()->getAnimationPropertyConfig("windowsMove"),
620	    AVARDAMAGE_NONE
621	);
622
623	this->updateWithGroup(node, true);
624	this->pos->warp();
625	this->size->warp();
626	this->bar.monitor_id = node.layout()->workspace()->m_monitor->m_id;
627}
628
629void Hy3TabGroup::updateWithGroup(Hy3Node& node, bool warp) {
630	static const auto bar_height = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:height");
631
632	auto tpos = node.visualBox.pos();
633	auto tsize = Vector2D(node.visualBox.w, *bar_height);
634
635	this->hidden = node.hidden;
636	if (this->pos->goal() != tpos) {
637		*this->pos = tpos;
638		if (warp) this->pos->warp();
639	}
640
641	if (this->size->goal() != tsize) {
642		*this->size = tsize;
643		if (warp) this->size->warp();
644	}
645
646	this->bar.updateNodeList(node.as_group().children);
647	this->bar.updateAnimations(warp);
648
649	auto locked = node.as_group().locked;
650	if (this->bar.locked->goal() != locked) *this->bar.locked = locked;
651
652	if (node.as_group().focused_child != nullptr) {
653		this->updateStencilWindows(*node.as_group().focused_child);
654	}
655
656	if (this->bar.dirty) this->tick();
657}
658
659void Hy3TabBar::damageBox(const Vector2D* position, const Vector2D* size) {
660	auto box = CBox {position->x, position->y, size->x, size->y};
661	// Either a rounding error or an issue below makes this necessary.
662	box.expand(1);
663	g_pHyprRenderer->damageBox(box);
664	this->damaged = true;
665}
666
667void Hy3TabGroup::tick() {
668	static const auto enter_from_top = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:from_top");
669	static const auto padding = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding");
670	static const auto no_gaps_when_only = CConfigValue<Config::INTEGER>("plugin:hy3:no_gaps_when_only");
671
672	this->bar.tick();
673
674	if (valid(this->workspace) && this->workspace->m_monitor) {
675		auto has_fullscreen = Fullscreen::controller()->hasFullscreen(this->workspace);
676
677		if (!has_fullscreen && *no_gaps_when_only) {
678			auto* hy3 = hy3InstanceForWorkspace(this->workspace);
679			auto root_node = hy3 ? hy3->getWorkspaceRootGroup(this->workspace.get()) : nullptr;
680			has_fullscreen = root_node != nullptr && root_node->as_group().children.size() == 1
681			              && root_node->as_group().children.front()->is_target();
682		}
683
684		if (has_fullscreen) {
685			if (this->bar.fade_opacity->goal() != 0.0) *this->bar.fade_opacity = 0.0;
686		} else {
687			if (this->bar.fade_opacity->goal() != 1.0) *this->bar.fade_opacity = 1.0;
688		}
689
690		if (bar.monitor_id != this->workspace->m_monitor->m_id) {
691			bar.monitor_id = this->workspace->m_monitor->m_id;
692			bar.dirty = true;
693		}
694
695		auto workspaceOffset = this->workspace->m_renderOffset->value();
696		if (this->last_workspace_offset != workspaceOffset) {
697			// First we damage the area where the bar was during the previous
698			// tick, cleaning up after ourselves
699			auto pos = this->last_pos + this->last_workspace_offset;
700			auto size = this->last_size;
701			this->bar.damageBox(&pos, &size);
702
703			// Then we damage the current position of the bar, to avoid seeing
704			// glitches with animations disabled
705			pos = this->pos->value() + workspaceOffset;
706			size = this->size->value();
707			this->bar.damageBox(&pos, &size);
708
709			this->last_workspace_offset = workspaceOffset;
710		}
711
712		if (this->workspace->m_alpha->isBeingAnimated()) {
713			auto pos = this->pos->value();
714			auto size = this->size->value();
715			this->bar.damageBox(&pos, &size);
716		}
717	}
718
719	auto pos = this->pos->value();
720	auto size = this->size->value();
721
722	if (this->last_pos != pos || this->last_size != size) {
723		this->bar.damageBox(&this->last_pos, &this->last_size);
724		this->last_pos = pos;
725		this->last_size = size;
726	}
727
728	if (this->bar.destroy || this->bar.dirty) {
729		// damage any area that could be covered by bar in/out animations
730		size.y = size.y * 2 + *padding;
731		if (*enter_from_top) {
732			pos.y -= *padding;
733		}
734
735		this->bar.damageBox(&pos, &size);
736		this->bar.dirty = false;
737	}
738}
739
740std::pair<CBox, CBox> Hy3TabGroup::getRenderBB(Render::CRenderContext& ctx) const {
741	auto* monitor = ctx.m_data.pMonitor.get();
742	auto scale = monitor->m_scale;
743
744	auto pos = this->pos->value() - monitor->m_position;
745	auto size = this->size->value();
746
747	if (valid(this->workspace)) {
748		pos = pos + this->workspace->m_renderOffset->value();
749	}
750
751	auto box = CBox(pos, size);
752	auto scaledBox = box.copy().scale(scale);
753
754	return std::make_pair(box, scaledBox);
755}
756
757void Hy3TabGroup::renderTabBar(Render::CRenderContext& ctx) {
758	static const auto window_rounding = CConfigValue<Config::INTEGER>("decoration:rounding");
759	static const auto enter_from_top = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:from_top");
760	static const auto padding = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding");
761	static const auto padding_horizontal =
762	    CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding_horizontal");
763	static const auto padding_outer = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:padding_outer");
764
765	auto [box, scaledBox] = this->getRenderBB(ctx);
766
767	auto* monitor = ctx.m_data.pMonitor.get();
768	auto scale = monitor->m_scale;
769
770	if (!this->bar.damaged) {
771		pixman_region32 damage;
772		pixman_region32_init(&damage);
773
774		pixman_region32_intersect_rect(
775		    &damage,
776		    ctx.m_data.damage.pixman(),
777		    scaledBox.x,
778		    scaledBox.y,
779		    scaledBox.width,
780		    scaledBox.height
781		);
782
783		this->bar.damaged = pixman_region32_not_empty(&damage);
784		pixman_region32_fini(&damage);
785	}
786
787	if (!this->bar.damaged || this->bar.destroy) return;
788	this->bar.damaged = false;
789
790	this->bar.setSize(scaledBox.size());
791
792	auto render_stencil = this->bar.fade_opacity->isBeingAnimated();
793
794	if (!render_stencil) {
795		for (auto& entry: this->bar.entries) {
796			if (entry.vertical_pos->isBeingAnimated()) {
797				render_stencil = true;
798				break;
799			}
800		}
801	}
802
803	if (render_stencil) {
804		ctx.m_data.currentFB->bind();
805		glEnable(GL_STENCIL_TEST);
806		glStencilMask(0xff);
807		glClearStencil(0);
808		glClear(GL_STENCIL_BUFFER_BIT);
809		glStencilFunc(GL_ALWAYS, 1, 0xff);
810		glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
811
812		glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
813
814		for (auto windowref: this->stencil_windows) {
815			if (!valid(windowref)) continue;
816			auto window = windowref.lock();
817
818			auto wpos = window->position(IGeometric::GEOMETRIC_CURRENT) - monitor->m_position
819			          + (window->m_workspace ? window->m_workspace->m_renderOffset->value() : Vector2D());
820
821			auto wsize = window->size(IGeometric::GEOMETRIC_CURRENT);
822
823			CBox window_box = {wpos.x, wpos.y, wsize.x, wsize.y};
824			auto border = window->presentation().borderSize();
825			auto radius = sc<int>(*window_rounding) + border;
826			window_box.expand(border);
827			// scaleBox(&window_box, scale);
828			window_box.scale(scale);
829
830			if (window_box.width > 0 && window_box.height > 0)
831				Render::GL::g_pHyprOpenGL->renderRect(ctx, window_box, CHyprColor(0, 0, 0, 0), { .round = radius });
832		}
833
834		glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
835
836		glStencilMask(0x00);
837		glStencilFunc(GL_EQUAL, 0, 0xff);
838		glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
839	}
840
841	auto fade_opacity = this->bar.fade_opacity->value()
842	                  * (valid(this->workspace) ? this->workspace->m_alpha->value() : 1.0);
843	auto horizontal_padding = *padding_horizontal < 0 ? *padding : *padding_horizontal;
844
845	auto render_entry = [&](Hy3TabBarEntry& entry) {
846		auto [entry_x, entry_width] = tab_entry_geometry(
847		    box.w,
848		    horizontal_padding,
849		    *padding_outer,
850		    entry.offset->value(),
851		    entry.width->value()
852		);
853		Vector2D entry_pos = {
854		    (box.x + entry_x) * scale,
855		    scaledBox.y
856		        + ((entry.vertical_pos->value() * (box.h + *padding) * scale)
857		           * (*enter_from_top ? -1 : 1)),
858		};
859		Vector2D entry_size = {
860		    entry_width * scale,
861		    scaledBox.h,
862		};
863		if (entry_size.x < 0 || entry_size.y < 0 || fade_opacity == 0.0) return;
864
865		CBox box = {
866		    entry_pos.x,
867		    entry_pos.y,
868		    entry_size.x,
869		    entry_size.y,
870		};
871
872		box.round();
873		entry.render(ctx, scale, box, fade_opacity);
874	};
875
876	for (auto& entry: this->bar.entries) {
877		if (entry.focused->goal() == 1.0) continue;
878		render_entry(entry);
879	}
880
881	for (auto& entry: this->bar.entries) {
882		if (entry.focused->goal() == 0.0) continue;
883		render_entry(entry);
884	}
885
886	if (render_stencil) {
887		glClearStencil(0);
888		glStencilMask(0xff);
889		glClear(GL_STENCIL_BUFFER_BIT);
890		glDisable(GL_STENCIL_TEST);
891		glStencilFunc(GL_ALWAYS, 1, 0xff);
892	}
893}
894
895std::vector<UP<IPassElement>> Hy3TabPassElement::draw(Render::CRenderContext& ctx) {
896	this->group->renderTabBar(ctx);
897	return {};
898}
899
900bool Hy3TabPassElement::needsPrecomputeBlur(Render::CRenderContext& ctx) {
901	// clang-format off
902	static const auto s_opacity = CConfigValue<Config::FLOAT>("plugin:hy3:tabs:opacity");
903	static const auto blur = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:blur");
904	static const auto col_active = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active");
905	static const auto col_border_active = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:active_border");
906	static const auto col_focused = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:focused");
907	static const auto col_border_focused = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:focused_border");
908	static const auto col_urgent = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:urgent");
909	static const auto col_border_urgent = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:urgent_border");
910	static const auto col_locked = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:locked");
911	static const auto col_border_locked = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:locked_border");
912	static const auto col_inactive = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:inactive");
913	static const auto col_border_inactive = CConfigValue<Config::INTEGER>("plugin:hy3:tabs:colors:inactive_border");
914	// clang-format on
915
916	if (!*blur) return false;
917	if (*s_opacity < 1.0) return false;
918
919	auto needsblur = [](const auto& col) { return CHyprColor(*col).a < 1.0; };
920	return needsblur(col_active) || needsblur(col_border_active) || needsblur(col_focused)
921	    || needsblur(col_border_focused) || needsblur(col_urgent) || needsblur(col_border_urgent)
922	    || needsblur(col_locked) || needsblur(col_border_locked) || needsblur(col_inactive)
923	    || needsblur(col_border_inactive);
924}
925
926std::optional<CBox> Hy3TabPassElement::boundingBox(Render::CRenderContext& ctx) {
927	return this->group->getRenderBB(ctx).first;
928}
929
930void findOverlappingWindows(Hy3Node& node, float height, std::vector<PHLWINDOWREF>& windows) {
931	switch (node.type()) {
932	case Hy3NodeType::Target: windows.push_back(node.as_window()); break;
933	case Hy3NodeType::Group:
934		auto& group = node.as_group();
935
936		switch (group.layout) {
937		case Hy3GroupLayout::Root:
938		case Hy3GroupLayout::SplitH:
939			for (auto& node: group.children) {
940				findOverlappingWindows(*node, height, windows);
941			}
942			break;
943		case Hy3GroupLayout::SplitV:
944			for (auto& node: group.children) {
945				findOverlappingWindows(*node, height, windows);
946				height -= node->visualBox.h;
947				if (height <= 0) break;
948			}
949			break;
950		case Hy3GroupLayout::Tabbed:
951			// assume the height of that node's tab bar already pushes it out of range
952			break;
953		}
954	}
955}
956
957void Hy3TabGroup::updateStencilWindows(Hy3Node& group) {
958	this->stencil_windows.clear();
959	findOverlappingWindows(group, this->size->goal().y, this->stencil_windows);
960}