Parent directory

TabGroup.cpp

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