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}