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}