1/**************************************************************************/
2/* tab_bar.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "tab_bar.h"
32
33#include "core/object/message_queue.h"
34#include "core/string/translation.h"
35#include "scene/gui/box_container.h"
36#include "scene/gui/label.h"
37#include "scene/gui/texture_rect.h"
38#include "scene/main/viewport.h"
39#include "scene/theme/theme_db.h"
40
41Size2 TabBar::get_minimum_size() const {
42 Size2 ms;
43
44 if (tabs.is_empty()) {
45 return ms;
46 }
47
48 int y_margin = MAX(MAX(MAX(theme_cache.tab_unselected_style->get_minimum_size().height, theme_cache.tab_hovered_style->get_minimum_size().height), theme_cache.tab_selected_style->get_minimum_size().height), theme_cache.tab_disabled_style->get_minimum_size().height);
49
50 for (int i = 0; i < tabs.size(); i++) {
51 if (tabs[i].hidden) {
52 continue;
53 }
54
55 int ofs = ms.width;
56
57 Ref<StyleBox> style;
58 if (tabs[i].disabled) {
59 style = theme_cache.tab_disabled_style;
60 } else if (current == i) {
61 style = theme_cache.tab_selected_style;
62 } else if (hover == i) {
63 style = theme_cache.tab_hovered_style;
64 } else {
65 style = theme_cache.tab_unselected_style;
66 }
67 ms.width += style->get_minimum_size().width;
68
69 if (tabs[i].icon.is_valid()) {
70 const Size2 icon_size = _get_tab_icon_size(i);
71 ms.height = MAX(ms.height, icon_size.height + y_margin);
72 ms.width += icon_size.width + theme_cache.h_separation;
73 }
74
75 if (!tabs[i].text.is_empty()) {
76 ms.width += tabs[i].size_text + theme_cache.h_separation;
77 }
78 ms.height = MAX(ms.height, tabs[i].text_buf->get_size().y + y_margin);
79
80 bool close_visible = cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current);
81
82 if (tabs[i].right_button.is_valid()) {
83 Ref<Texture2D> rb = tabs[i].right_button;
84
85 if (close_visible) {
86 ms.width += theme_cache.button_hl_style->get_minimum_size().width + rb->get_width();
87 } else {
88 ms.width += theme_cache.button_hl_style->get_margin(SIDE_LEFT) + rb->get_width() + theme_cache.h_separation;
89 }
90
91 ms.height = MAX(ms.height, rb->get_height() + y_margin);
92 }
93
94 if (close_visible) {
95 ms.width += theme_cache.button_hl_style->get_margin(SIDE_LEFT) + theme_cache.close_icon->get_width() + theme_cache.h_separation;
96
97 ms.height = MAX(ms.height, theme_cache.close_icon->get_height() + y_margin);
98 }
99
100 if (ms.width - ofs > style->get_minimum_size().width) {
101 ms.width -= theme_cache.h_separation;
102 }
103 }
104
105 if (clip_tabs) {
106 ms.width = 0;
107 }
108
109 return ms;
110}
111
112void TabBar::gui_input(const Ref<InputEvent> &p_event) {
113 ERR_FAIL_COND(p_event.is_null());
114
115 Ref<InputEventMouseMotion> mm = p_event;
116
117 if (mm.is_valid()) {
118 Point2 pos = mm->get_position();
119
120 if (buttons_visible) {
121 if (is_layout_rtl()) {
122 if (pos.x < theme_cache.decrement_icon->get_width()) {
123 if (highlight_arrow != 1) {
124 highlight_arrow = 1;
125 queue_redraw();
126 }
127 } else if (pos.x < theme_cache.increment_icon->get_width() + theme_cache.decrement_icon->get_width()) {
128 if (highlight_arrow != 0) {
129 highlight_arrow = 0;
130 queue_redraw();
131 }
132 } else if (highlight_arrow != -1) {
133 highlight_arrow = -1;
134 queue_redraw();
135 }
136 } else {
137 int limit_minus_buttons = get_size().width - theme_cache.increment_icon->get_width() - theme_cache.decrement_icon->get_width();
138 if (pos.x > limit_minus_buttons + theme_cache.decrement_icon->get_width()) {
139 if (highlight_arrow != 1) {
140 highlight_arrow = 1;
141 queue_redraw();
142 }
143 } else if (pos.x > limit_minus_buttons) {
144 if (highlight_arrow != 0) {
145 highlight_arrow = 0;
146 queue_redraw();
147 }
148 } else if (highlight_arrow != -1) {
149 highlight_arrow = -1;
150 queue_redraw();
151 }
152 }
153 }
154
155 if (get_viewport()->gui_is_dragging() && can_drop_data(pos, get_viewport()->gui_get_drag_data())) {
156 dragging_valid_tab = true;
157 queue_redraw();
158 }
159
160 if (!tabs.is_empty()) {
161 _update_hover();
162 }
163
164 return;
165 }
166
167 Ref<InputEventMouseButton> mb = p_event;
168
169 if (mb.is_valid()) {
170 if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP && !mb->is_command_or_control_pressed()) {
171 if (scrolling_enabled && buttons_visible) {
172 if (offset > 0) {
173 offset--;
174 _update_cache();
175 queue_redraw();
176 }
177 }
178 }
179
180 if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN && !mb->is_command_or_control_pressed()) {
181 if (scrolling_enabled && buttons_visible) {
182 if (missing_right && offset < tabs.size()) {
183 offset++;
184 _update_cache();
185 queue_redraw();
186 }
187 }
188 }
189
190 if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
191 if (rb_hover != -1) {
192 emit_signal(SNAME("tab_button_pressed"), rb_hover);
193 }
194
195 rb_pressing = false;
196 queue_redraw();
197 }
198
199 if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
200 if (cb_hover != -1) {
201 emit_signal(SNAME("tab_close_pressed"), cb_hover);
202 }
203
204 cb_pressing = false;
205 queue_redraw();
206 }
207
208 if (mb->is_pressed() && (mb->get_button_index() == MouseButton::LEFT || (select_with_rmb && mb->get_button_index() == MouseButton::RIGHT))) {
209 Point2 pos = mb->get_position();
210
211 if (buttons_visible) {
212 if (is_layout_rtl()) {
213 if (pos.x < theme_cache.decrement_icon->get_width()) {
214 if (missing_right) {
215 offset++;
216 _update_cache();
217 queue_redraw();
218 }
219 return;
220 } else if (pos.x < theme_cache.increment_icon->get_width() + theme_cache.decrement_icon->get_width()) {
221 if (offset > 0) {
222 offset--;
223 _update_cache();
224 queue_redraw();
225 }
226 return;
227 }
228 } else {
229 int limit = get_size().width - theme_cache.increment_icon->get_width() - theme_cache.decrement_icon->get_width();
230 if (pos.x > limit + theme_cache.decrement_icon->get_width()) {
231 if (missing_right) {
232 offset++;
233 _update_cache();
234 queue_redraw();
235 }
236 return;
237 } else if (pos.x > limit) {
238 if (offset > 0) {
239 offset--;
240 _update_cache();
241 queue_redraw();
242 }
243 return;
244 }
245 }
246 }
247
248 if (tabs.is_empty()) {
249 // Return early if there are no actual tabs to handle input for.
250 return;
251 }
252
253 int found = -1;
254 for (int i = offset; i <= max_drawn_tab; i++) {
255 if (tabs[i].hidden) {
256 continue;
257 }
258
259 if (tabs[i].rb_rect.has_point(pos)) {
260 rb_pressing = true;
261 _update_hover();
262 queue_redraw();
263 return;
264 }
265
266 if (tabs[i].cb_rect.has_point(pos) && (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current))) {
267 cb_pressing = true;
268 _update_hover();
269 queue_redraw();
270 return;
271 }
272
273 if (pos.x >= get_tab_rect(i).position.x && pos.x < get_tab_rect(i).position.x + tabs[i].size_cache) {
274 if (!tabs[i].disabled) {
275 found = i;
276 }
277 break;
278 }
279 }
280
281 if (found != -1) {
282 set_current_tab(found);
283
284 if (mb->get_button_index() == MouseButton::RIGHT) {
285 // Right mouse button clicked.
286 emit_signal(SNAME("tab_rmb_clicked"), found);
287 }
288
289 emit_signal(SNAME("tab_clicked"), found);
290 }
291 }
292 }
293}
294
295void TabBar::_shape(int p_tab) {
296 tabs.write[p_tab].xl_text = atr(tabs[p_tab].text);
297 tabs.write[p_tab].text_buf->clear();
298 tabs.write[p_tab].text_buf->set_width(-1);
299 if (tabs[p_tab].text_direction == Control::TEXT_DIRECTION_INHERITED) {
300 tabs.write[p_tab].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
301 } else {
302 tabs.write[p_tab].text_buf->set_direction((TextServer::Direction)tabs[p_tab].text_direction);
303 }
304
305 tabs.write[p_tab].text_buf->add_string(tabs[p_tab].xl_text, theme_cache.font, theme_cache.font_size, tabs[p_tab].language);
306}
307
308void TabBar::_notification(int p_what) {
309 switch (p_what) {
310 case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
311 queue_redraw();
312 } break;
313
314 case NOTIFICATION_THEME_CHANGED:
315 case NOTIFICATION_TRANSLATION_CHANGED: {
316 for (int i = 0; i < tabs.size(); ++i) {
317 _shape(i);
318 }
319
320 queue_redraw();
321
322 [[fallthrough]];
323 }
324 case NOTIFICATION_RESIZED: {
325 int ofs_old = offset;
326 int max_old = max_drawn_tab;
327
328 _update_cache();
329 _ensure_no_over_offset();
330
331 if (scroll_to_selected && (offset != ofs_old || max_drawn_tab != max_old)) {
332 ensure_tab_visible(current);
333 }
334 } break;
335
336 case NOTIFICATION_DRAG_END: {
337 if (dragging_valid_tab) {
338 dragging_valid_tab = false;
339 queue_redraw();
340 }
341 } break;
342
343 case NOTIFICATION_DRAW: {
344 bool rtl = is_layout_rtl();
345 Vector2 size = get_size();
346
347 if (tabs.is_empty()) {
348 // Draw the drop indicator where the first tab would be if there are no tabs.
349 if (dragging_valid_tab) {
350 int x = rtl ? size.x : 0;
351 theme_cache.drop_mark_icon->draw(get_canvas_item(), Point2(x - (theme_cache.drop_mark_icon->get_width() / 2), (size.height - theme_cache.drop_mark_icon->get_height()) / 2), theme_cache.drop_mark_color);
352 }
353
354 return;
355 }
356
357 int limit_minus_buttons = size.width - theme_cache.increment_icon->get_width() - theme_cache.decrement_icon->get_width();
358
359 int ofs = tabs[offset].ofs_cache;
360
361 // Draw unselected tabs in the back.
362 for (int i = offset; i <= max_drawn_tab; i++) {
363 if (tabs[i].hidden) {
364 continue;
365 }
366
367 if (i != current) {
368 Ref<StyleBox> sb;
369 Color col;
370
371 if (tabs[i].disabled) {
372 sb = theme_cache.tab_disabled_style;
373 col = theme_cache.font_disabled_color;
374 } else if (i == hover) {
375 sb = theme_cache.tab_hovered_style;
376 col = theme_cache.font_hovered_color;
377 } else {
378 sb = theme_cache.tab_unselected_style;
379 col = theme_cache.font_unselected_color;
380 }
381
382 _draw_tab(sb, col, i, rtl ? size.width - ofs - tabs[i].size_cache : ofs);
383 }
384
385 ofs += tabs[i].size_cache;
386 }
387
388 // Draw selected tab in the front, but only if it's visible.
389 if (current >= offset && current <= max_drawn_tab && !tabs[current].hidden) {
390 Ref<StyleBox> sb = tabs[current].disabled ? theme_cache.tab_disabled_style : theme_cache.tab_selected_style;
391 float x = rtl ? size.width - tabs[current].ofs_cache - tabs[current].size_cache : tabs[current].ofs_cache;
392
393 _draw_tab(sb, theme_cache.font_selected_color, current, x);
394 }
395
396 if (buttons_visible) {
397 int vofs = (size.height - theme_cache.increment_icon->get_size().height) / 2;
398
399 if (rtl) {
400 if (missing_right) {
401 draw_texture(highlight_arrow == 1 ? theme_cache.decrement_hl_icon : theme_cache.decrement_icon, Point2(0, vofs));
402 } else {
403 draw_texture(theme_cache.decrement_icon, Point2(0, vofs), Color(1, 1, 1, 0.5));
404 }
405
406 if (offset > 0) {
407 draw_texture(highlight_arrow == 0 ? theme_cache.increment_hl_icon : theme_cache.increment_icon, Point2(theme_cache.increment_icon->get_size().width, vofs));
408 } else {
409 draw_texture(theme_cache.increment_icon, Point2(theme_cache.increment_icon->get_size().width, vofs), Color(1, 1, 1, 0.5));
410 }
411 } else {
412 if (offset > 0) {
413 draw_texture(highlight_arrow == 0 ? theme_cache.decrement_hl_icon : theme_cache.decrement_icon, Point2(limit_minus_buttons, vofs));
414 } else {
415 draw_texture(theme_cache.decrement_icon, Point2(limit_minus_buttons, vofs), Color(1, 1, 1, 0.5));
416 }
417
418 if (missing_right) {
419 draw_texture(highlight_arrow == 1 ? theme_cache.increment_hl_icon : theme_cache.increment_icon, Point2(limit_minus_buttons + theme_cache.decrement_icon->get_size().width, vofs));
420 } else {
421 draw_texture(theme_cache.increment_icon, Point2(limit_minus_buttons + theme_cache.decrement_icon->get_size().width, vofs), Color(1, 1, 1, 0.5));
422 }
423 }
424 }
425
426 if (dragging_valid_tab) {
427 int x;
428
429 int tab_hover = get_hovered_tab();
430 if (tab_hover != -1) {
431 Rect2 tab_rect = get_tab_rect(tab_hover);
432
433 x = tab_rect.position.x;
434 if (get_local_mouse_position().x > x + tab_rect.size.width / 2) {
435 x += tab_rect.size.width;
436 }
437 } else {
438 if (rtl ^ (get_local_mouse_position().x < get_tab_rect(0).position.x)) {
439 x = get_tab_rect(0).position.x;
440 if (rtl) {
441 x += get_tab_rect(0).size.width;
442 }
443 } else {
444 Rect2 tab_rect = get_tab_rect(get_tab_count() - 1);
445
446 x = tab_rect.position.x;
447 if (!rtl) {
448 x += tab_rect.size.width;
449 }
450 }
451 }
452
453 theme_cache.drop_mark_icon->draw(get_canvas_item(), Point2(x - theme_cache.drop_mark_icon->get_width() / 2, (size.height - theme_cache.drop_mark_icon->get_height()) / 2), theme_cache.drop_mark_color);
454 }
455 } break;
456 }
457}
458
459void TabBar::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x) {
460 RID ci = get_canvas_item();
461 bool rtl = is_layout_rtl();
462
463 Rect2 sb_rect = Rect2(p_x, 0, tabs[p_index].size_cache, get_size().height);
464 p_tab_style->draw(ci, sb_rect);
465
466 p_x += rtl ? tabs[p_index].size_cache - p_tab_style->get_margin(SIDE_LEFT) : p_tab_style->get_margin(SIDE_LEFT);
467
468 Size2i sb_ms = p_tab_style->get_minimum_size();
469
470 // Draw the icon.
471 Ref<Texture2D> icon = tabs[p_index].icon;
472 if (icon.is_valid()) {
473 const Size2 icon_size = _get_tab_icon_size(p_index);
474 const Point2 icon_pos = Point2i(rtl ? p_x - icon_size.width : p_x, p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon_size.height) / 2);
475 icon->draw_rect(ci, Rect2(icon_pos, icon_size));
476
477 p_x = rtl ? p_x - icon_size.width - theme_cache.h_separation : p_x + icon_size.width + theme_cache.h_separation;
478 }
479
480 // Draw the text.
481 if (!tabs[p_index].text.is_empty()) {
482 Point2i text_pos = Point2i(rtl ? p_x - tabs[p_index].size_text : p_x,
483 p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[p_index].text_buf->get_size().y) / 2);
484
485 if (theme_cache.outline_size > 0 && theme_cache.font_outline_color.a > 0) {
486 tabs[p_index].text_buf->draw_outline(ci, text_pos, theme_cache.outline_size, theme_cache.font_outline_color);
487 }
488 tabs[p_index].text_buf->draw(ci, text_pos, p_font_color);
489
490 p_x = rtl ? p_x - tabs[p_index].size_text - theme_cache.h_separation : p_x + tabs[p_index].size_text + theme_cache.h_separation;
491 }
492
493 // Draw and calculate rect of the right button.
494 if (tabs[p_index].right_button.is_valid()) {
495 Ref<StyleBox> style = theme_cache.button_hl_style;
496 Ref<Texture2D> rb = tabs[p_index].right_button;
497
498 Rect2 rb_rect;
499 rb_rect.size = style->get_minimum_size() + rb->get_size();
500 rb_rect.position.x = rtl ? p_x - rb_rect.size.width : p_x;
501 rb_rect.position.y = p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (rb_rect.size.y)) / 2;
502
503 tabs.write[p_index].rb_rect = rb_rect;
504
505 if (rb_hover == p_index) {
506 if (rb_pressing) {
507 theme_cache.button_pressed_style->draw(ci, rb_rect);
508 } else {
509 style->draw(ci, rb_rect);
510 }
511 }
512
513 rb->draw(ci, Point2i(rb_rect.position.x + style->get_margin(SIDE_LEFT), rb_rect.position.y + style->get_margin(SIDE_TOP)));
514
515 p_x = rtl ? rb_rect.position.x : rb_rect.position.x + rb_rect.size.width;
516 }
517
518 // Draw and calculate rect of the close button.
519 if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && p_index == current)) {
520 Ref<StyleBox> style = theme_cache.button_hl_style;
521 Ref<Texture2D> cb = theme_cache.close_icon;
522
523 Rect2 cb_rect;
524 cb_rect.size = style->get_minimum_size() + cb->get_size();
525 cb_rect.position.x = rtl ? p_x - cb_rect.size.width : p_x;
526 cb_rect.position.y = p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (cb_rect.size.y)) / 2;
527
528 tabs.write[p_index].cb_rect = cb_rect;
529
530 if (!tabs[p_index].disabled && cb_hover == p_index) {
531 if (cb_pressing) {
532 theme_cache.button_pressed_style->draw(ci, cb_rect);
533 } else {
534 style->draw(ci, cb_rect);
535 }
536 }
537
538 cb->draw(ci, Point2i(cb_rect.position.x + style->get_margin(SIDE_LEFT), cb_rect.position.y + style->get_margin(SIDE_TOP)));
539 }
540}
541
542void TabBar::set_tab_count(int p_count) {
543 if (p_count == tabs.size()) {
544 return;
545 }
546
547 ERR_FAIL_COND(p_count < 0);
548 tabs.resize(p_count);
549
550 if (p_count == 0) {
551 offset = 0;
552 max_drawn_tab = 0;
553 current = 0;
554 previous = 0;
555 } else {
556 offset = MIN(offset, p_count - 1);
557 max_drawn_tab = MIN(max_drawn_tab, p_count - 1);
558 current = MIN(current, p_count - 1);
559
560 _update_cache();
561 _ensure_no_over_offset();
562 if (scroll_to_selected) {
563 ensure_tab_visible(current);
564 }
565 }
566
567 queue_redraw();
568 update_minimum_size();
569 notify_property_list_changed();
570}
571
572int TabBar::get_tab_count() const {
573 return tabs.size();
574}
575
576void TabBar::set_current_tab(int p_current) {
577 ERR_FAIL_INDEX(p_current, get_tab_count());
578
579 previous = current;
580 current = p_current;
581
582 if (current == previous) {
583 emit_signal(SNAME("tab_selected"), current);
584 return;
585 }
586
587 emit_signal(SNAME("tab_selected"), current);
588
589 _update_cache();
590 if (scroll_to_selected) {
591 ensure_tab_visible(current);
592 }
593 queue_redraw();
594
595 emit_signal(SNAME("tab_changed"), p_current);
596}
597
598int TabBar::get_current_tab() const {
599 return current;
600}
601
602int TabBar::get_previous_tab() const {
603 return previous;
604}
605
606int TabBar::get_hovered_tab() const {
607 return hover;
608}
609
610int TabBar::get_tab_offset() const {
611 return offset;
612}
613
614bool TabBar::get_offset_buttons_visible() const {
615 return buttons_visible;
616}
617
618void TabBar::set_tab_title(int p_tab, const String &p_title) {
619 ERR_FAIL_INDEX(p_tab, tabs.size());
620
621 if (tabs[p_tab].text == p_title) {
622 return;
623 }
624
625 tabs.write[p_tab].text = p_title;
626
627 _shape(p_tab);
628 _update_cache();
629 _ensure_no_over_offset();
630 if (scroll_to_selected) {
631 ensure_tab_visible(current);
632 }
633 queue_redraw();
634 update_minimum_size();
635}
636
637String TabBar::get_tab_title(int p_tab) const {
638 ERR_FAIL_INDEX_V(p_tab, tabs.size(), "");
639 return tabs[p_tab].text;
640}
641
642void TabBar::set_tab_text_direction(int p_tab, Control::TextDirection p_text_direction) {
643 ERR_FAIL_INDEX(p_tab, tabs.size());
644 ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3);
645
646 if (tabs[p_tab].text_direction != p_text_direction) {
647 tabs.write[p_tab].text_direction = p_text_direction;
648 _shape(p_tab);
649 queue_redraw();
650 }
651}
652
653Control::TextDirection TabBar::get_tab_text_direction(int p_tab) const {
654 ERR_FAIL_INDEX_V(p_tab, tabs.size(), Control::TEXT_DIRECTION_INHERITED);
655 return tabs[p_tab].text_direction;
656}
657
658void TabBar::set_tab_language(int p_tab, const String &p_language) {
659 ERR_FAIL_INDEX(p_tab, tabs.size());
660
661 if (tabs[p_tab].language != p_language) {
662 tabs.write[p_tab].language = p_language;
663 _shape(p_tab);
664 _update_cache();
665 _ensure_no_over_offset();
666 if (scroll_to_selected) {
667 ensure_tab_visible(current);
668 }
669 queue_redraw();
670 update_minimum_size();
671 }
672}
673
674String TabBar::get_tab_language(int p_tab) const {
675 ERR_FAIL_INDEX_V(p_tab, tabs.size(), "");
676 return tabs[p_tab].language;
677}
678
679void TabBar::set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon) {
680 ERR_FAIL_INDEX(p_tab, tabs.size());
681
682 if (tabs[p_tab].icon == p_icon) {
683 return;
684 }
685
686 tabs.write[p_tab].icon = p_icon;
687
688 _update_cache();
689 _ensure_no_over_offset();
690 if (scroll_to_selected) {
691 ensure_tab_visible(current);
692 }
693 queue_redraw();
694 update_minimum_size();
695}
696
697Ref<Texture2D> TabBar::get_tab_icon(int p_tab) const {
698 ERR_FAIL_INDEX_V(p_tab, tabs.size(), Ref<Texture2D>());
699 return tabs[p_tab].icon;
700}
701
702void TabBar::set_tab_icon_max_width(int p_tab, int p_width) {
703 ERR_FAIL_INDEX(p_tab, tabs.size());
704
705 if (tabs[p_tab].icon_max_width == p_width) {
706 return;
707 }
708
709 tabs.write[p_tab].icon_max_width = p_width;
710
711 _update_cache();
712 _ensure_no_over_offset();
713 if (scroll_to_selected) {
714 ensure_tab_visible(current);
715 }
716 queue_redraw();
717 update_minimum_size();
718}
719
720int TabBar::get_tab_icon_max_width(int p_tab) const {
721 ERR_FAIL_INDEX_V(p_tab, tabs.size(), 0);
722 return tabs[p_tab].icon_max_width;
723}
724
725void TabBar::set_tab_disabled(int p_tab, bool p_disabled) {
726 ERR_FAIL_INDEX(p_tab, tabs.size());
727
728 if (tabs[p_tab].disabled == p_disabled) {
729 return;
730 }
731
732 tabs.write[p_tab].disabled = p_disabled;
733
734 _update_cache();
735 _ensure_no_over_offset();
736 if (scroll_to_selected) {
737 ensure_tab_visible(current);
738 }
739 queue_redraw();
740 update_minimum_size();
741}
742
743bool TabBar::is_tab_disabled(int p_tab) const {
744 ERR_FAIL_INDEX_V(p_tab, tabs.size(), false);
745 return tabs[p_tab].disabled;
746}
747
748void TabBar::set_tab_hidden(int p_tab, bool p_hidden) {
749 ERR_FAIL_INDEX(p_tab, tabs.size());
750
751 if (tabs[p_tab].hidden == p_hidden) {
752 return;
753 }
754
755 tabs.write[p_tab].hidden = p_hidden;
756
757 _update_cache();
758 _ensure_no_over_offset();
759 if (scroll_to_selected) {
760 ensure_tab_visible(current);
761 }
762 queue_redraw();
763 update_minimum_size();
764}
765
766bool TabBar::is_tab_hidden(int p_tab) const {
767 ERR_FAIL_INDEX_V(p_tab, tabs.size(), false);
768 return tabs[p_tab].hidden;
769}
770
771void TabBar::set_tab_metadata(int p_tab, const Variant &p_metadata) {
772 ERR_FAIL_INDEX(p_tab, tabs.size());
773
774 if (tabs[p_tab].metadata == p_metadata) {
775 return;
776 }
777
778 tabs.write[p_tab].metadata = p_metadata;
779}
780
781Variant TabBar::get_tab_metadata(int p_tab) const {
782 ERR_FAIL_INDEX_V(p_tab, tabs.size(), Variant());
783 return tabs[p_tab].metadata;
784}
785
786void TabBar::set_tab_button_icon(int p_tab, const Ref<Texture2D> &p_icon) {
787 ERR_FAIL_INDEX(p_tab, tabs.size());
788
789 if (tabs[p_tab].right_button == p_icon) {
790 return;
791 }
792
793 tabs.write[p_tab].right_button = p_icon;
794
795 _update_cache();
796 _ensure_no_over_offset();
797 if (scroll_to_selected) {
798 ensure_tab_visible(current);
799 }
800 queue_redraw();
801 update_minimum_size();
802}
803
804Ref<Texture2D> TabBar::get_tab_button_icon(int p_tab) const {
805 ERR_FAIL_INDEX_V(p_tab, tabs.size(), Ref<Texture2D>());
806 return tabs[p_tab].right_button;
807}
808
809void TabBar::_update_hover() {
810 if (!is_inside_tree()) {
811 return;
812 }
813
814 ERR_FAIL_COND(tabs.is_empty());
815
816 const Point2 &pos = get_local_mouse_position();
817 // Test hovering to display right or close button.
818 int hover_now = -1;
819 int hover_buttons = -1;
820 for (int i = offset; i <= max_drawn_tab; i++) {
821 if (tabs[i].hidden) {
822 continue;
823 }
824
825 Rect2 rect = get_tab_rect(i);
826 if (rect.has_point(pos)) {
827 hover_now = i;
828 }
829
830 if (tabs[i].rb_rect.has_point(pos)) {
831 rb_hover = i;
832 cb_hover = -1;
833 hover_buttons = i;
834 } else if (!tabs[i].disabled && tabs[i].cb_rect.has_point(pos)) {
835 cb_hover = i;
836 rb_hover = -1;
837 hover_buttons = i;
838 }
839
840 if (hover_buttons != -1) {
841 queue_redraw();
842 break;
843 }
844 }
845
846 if (hover != hover_now) {
847 hover = hover_now;
848
849 if (hover != -1) {
850 emit_signal(SNAME("tab_hovered"), hover);
851 }
852
853 _update_cache();
854 queue_redraw();
855 }
856
857 if (hover_buttons == -1) { // No hover.
858 int rb_hover_old = rb_hover;
859 int cb_hover_old = cb_hover;
860
861 rb_hover = hover_buttons;
862 cb_hover = hover_buttons;
863
864 if (rb_hover != rb_hover_old || cb_hover != cb_hover_old) {
865 queue_redraw();
866 }
867 }
868}
869
870void TabBar::_update_cache() {
871 if (tabs.is_empty()) {
872 buttons_visible = false;
873 return;
874 }
875
876 int limit = get_size().width;
877 int limit_minus_buttons = limit - theme_cache.increment_icon->get_width() - theme_cache.decrement_icon->get_width();
878
879 int w = 0;
880
881 max_drawn_tab = tabs.size() - 1;
882
883 for (int i = 0; i < tabs.size(); i++) {
884 tabs.write[i].text_buf->set_width(-1);
885 tabs.write[i].size_text = Math::ceil(tabs[i].text_buf->get_size().x);
886 tabs.write[i].size_cache = get_tab_width(i);
887
888 if (max_width > 0 && tabs[i].size_cache > max_width) {
889 int size_textless = tabs[i].size_cache - tabs[i].size_text;
890 int mw = MAX(size_textless, max_width);
891
892 tabs.write[i].size_text = MAX(mw - size_textless, 1);
893 tabs.write[i].text_buf->set_width(tabs[i].size_text);
894 tabs.write[i].size_cache = size_textless + tabs[i].size_text;
895 }
896
897 if (i < offset || i > max_drawn_tab) {
898 tabs.write[i].ofs_cache = 0;
899 continue;
900 }
901
902 tabs.write[i].ofs_cache = w;
903
904 if (tabs[i].hidden) {
905 continue;
906 }
907
908 w += tabs[i].size_cache;
909
910 // Check if all tabs would fit inside the area.
911 if (clip_tabs && i > offset && (w > limit || (offset > 0 && w > limit_minus_buttons))) {
912 tabs.write[i].ofs_cache = 0;
913
914 w -= tabs[i].size_cache;
915 max_drawn_tab = i - 1;
916
917 while (w > limit_minus_buttons && max_drawn_tab > offset) {
918 tabs.write[max_drawn_tab].ofs_cache = 0;
919
920 if (!tabs[max_drawn_tab].hidden) {
921 w -= tabs[max_drawn_tab].size_cache;
922 }
923
924 max_drawn_tab--;
925 }
926 }
927 }
928
929 missing_right = max_drawn_tab < tabs.size() - 1;
930 buttons_visible = offset > 0 || missing_right;
931
932 if (tab_alignment == ALIGNMENT_LEFT) {
933 _update_hover();
934 return;
935 }
936
937 if (tab_alignment == ALIGNMENT_CENTER) {
938 w = ((buttons_visible ? limit_minus_buttons : limit) - w) / 2;
939 } else if (tab_alignment == ALIGNMENT_RIGHT) {
940 w = (buttons_visible ? limit_minus_buttons : limit) - w;
941 }
942
943 for (int i = offset; i <= max_drawn_tab; i++) {
944 tabs.write[i].ofs_cache = w;
945
946 if (!tabs[i].hidden) {
947 w += tabs[i].size_cache;
948 }
949 }
950
951 _update_hover();
952}
953
954void TabBar::_on_mouse_exited() {
955 rb_hover = -1;
956 cb_hover = -1;
957 hover = -1;
958 highlight_arrow = -1;
959 dragging_valid_tab = false;
960
961 _update_cache();
962 queue_redraw();
963}
964
965void TabBar::add_tab(const String &p_str, const Ref<Texture2D> &p_icon) {
966 Tab t;
967 t.text = p_str;
968 t.text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
969 t.icon = p_icon;
970 tabs.push_back(t);
971
972 _shape(tabs.size() - 1);
973 _update_cache();
974 if (scroll_to_selected) {
975 ensure_tab_visible(current);
976 }
977 queue_redraw();
978 update_minimum_size();
979
980 if (tabs.size() == 1 && is_inside_tree()) {
981 emit_signal(SNAME("tab_changed"), 0);
982 }
983}
984
985void TabBar::clear_tabs() {
986 if (tabs.is_empty()) {
987 return;
988 }
989
990 tabs.clear();
991 offset = 0;
992 max_drawn_tab = 0;
993 current = 0;
994 previous = 0;
995
996 queue_redraw();
997 update_minimum_size();
998 notify_property_list_changed();
999}
1000
1001void TabBar::remove_tab(int p_idx) {
1002 ERR_FAIL_INDEX(p_idx, tabs.size());
1003 tabs.remove_at(p_idx);
1004
1005 bool is_tab_changing = current == p_idx && !tabs.is_empty();
1006
1007 if (current >= p_idx && current > 0) {
1008 current--;
1009 }
1010
1011 if (tabs.is_empty()) {
1012 offset = 0;
1013 max_drawn_tab = 0;
1014 previous = 0;
1015 } else {
1016 offset = MIN(offset, tabs.size() - 1);
1017 max_drawn_tab = MIN(max_drawn_tab, tabs.size() - 1);
1018
1019 _update_cache();
1020 _ensure_no_over_offset();
1021 if (scroll_to_selected) {
1022 ensure_tab_visible(current);
1023 }
1024 }
1025
1026 queue_redraw();
1027 update_minimum_size();
1028 notify_property_list_changed();
1029
1030 if (is_tab_changing && is_inside_tree()) {
1031 emit_signal(SNAME("tab_changed"), current);
1032 }
1033}
1034
1035Variant TabBar::get_drag_data(const Point2 &p_point) {
1036 if (!drag_to_rearrange_enabled) {
1037 return Control::get_drag_data(p_point); // Allow stuff like TabContainer to override it.
1038 }
1039
1040 int tab_over = get_tab_idx_at_point(p_point);
1041 if (tab_over < 0) {
1042 return Variant();
1043 }
1044
1045 HBoxContainer *drag_preview = memnew(HBoxContainer);
1046
1047 if (!tabs[tab_over].icon.is_null()) {
1048 const Size2 icon_size = _get_tab_icon_size(tab_over);
1049
1050 TextureRect *tf = memnew(TextureRect);
1051 tf->set_texture(tabs[tab_over].icon);
1052 tf->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
1053 tf->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
1054 tf->set_custom_minimum_size(icon_size);
1055
1056 drag_preview->add_child(tf);
1057 }
1058
1059 Label *label = memnew(Label(tabs[tab_over].xl_text));
1060 drag_preview->add_child(label);
1061
1062 set_drag_preview(drag_preview);
1063
1064 Dictionary drag_data;
1065 drag_data["type"] = "tab_element";
1066 drag_data["tab_element"] = tab_over;
1067 drag_data["from_path"] = get_path();
1068
1069 return drag_data;
1070}
1071
1072bool TabBar::can_drop_data(const Point2 &p_point, const Variant &p_data) const {
1073 if (!drag_to_rearrange_enabled) {
1074 return Control::can_drop_data(p_point, p_data); // Allow stuff like TabContainer to override it.
1075 }
1076
1077 Dictionary d = p_data;
1078 if (!d.has("type")) {
1079 return false;
1080 }
1081
1082 if (String(d["type"]) == "tab_element") {
1083 NodePath from_path = d["from_path"];
1084 NodePath to_path = get_path();
1085 if (from_path == to_path) {
1086 return true;
1087 } else if (get_tabs_rearrange_group() != -1) {
1088 // Drag and drop between other TabBars.
1089 Node *from_node = get_node(from_path);
1090 TabBar *from_tabs = Object::cast_to<TabBar>(from_node);
1091 if (from_tabs && from_tabs->get_tabs_rearrange_group() == get_tabs_rearrange_group()) {
1092 return true;
1093 }
1094 }
1095 }
1096
1097 return false;
1098}
1099
1100void TabBar::drop_data(const Point2 &p_point, const Variant &p_data) {
1101 if (!drag_to_rearrange_enabled) {
1102 Control::drop_data(p_point, p_data); // Allow stuff like TabContainer to override it.
1103 return;
1104 }
1105
1106 Dictionary d = p_data;
1107 if (!d.has("type")) {
1108 return;
1109 }
1110
1111 if (String(d["type"]) == "tab_element") {
1112 int tab_from_id = d["tab_element"];
1113 int hover_now = get_tab_idx_at_point(p_point);
1114 NodePath from_path = d["from_path"];
1115 NodePath to_path = get_path();
1116
1117 if (from_path == to_path) {
1118 if (tab_from_id == hover_now) {
1119 return;
1120 }
1121
1122 // Drop the new tab to the left or right depending on where the target tab is being hovered.
1123 if (hover_now != -1) {
1124 Rect2 tab_rect = get_tab_rect(hover_now);
1125 if (is_layout_rtl() ^ (p_point.x <= tab_rect.position.x + tab_rect.size.width / 2)) {
1126 if (hover_now > tab_from_id) {
1127 hover_now -= 1;
1128 }
1129 } else if (tab_from_id > hover_now) {
1130 hover_now += 1;
1131 }
1132 } else {
1133 int x = tabs.is_empty() ? 0 : get_tab_rect(0).position.x;
1134 hover_now = is_layout_rtl() ^ (p_point.x < x) ? 0 : get_tab_count() - 1;
1135 }
1136
1137 move_tab(tab_from_id, hover_now);
1138 if (!is_tab_disabled(hover_now)) {
1139 emit_signal(SNAME("active_tab_rearranged"), hover_now);
1140 set_current_tab(hover_now);
1141 }
1142 } else if (get_tabs_rearrange_group() != -1) {
1143 // Drag and drop between Tabs.
1144
1145 Node *from_node = get_node(from_path);
1146 TabBar *from_tabs = Object::cast_to<TabBar>(from_node);
1147
1148 if (from_tabs && from_tabs->get_tabs_rearrange_group() == get_tabs_rearrange_group()) {
1149 if (tab_from_id >= from_tabs->get_tab_count()) {
1150 return;
1151 }
1152
1153 // Drop the new tab to the left or right depending on where the target tab is being hovered.
1154 if (hover_now != -1) {
1155 Rect2 tab_rect = get_tab_rect(hover_now);
1156 if (is_layout_rtl() ^ (p_point.x > tab_rect.position.x + tab_rect.size.width / 2)) {
1157 hover_now += 1;
1158 }
1159 } else {
1160 hover_now = tabs.is_empty() || (is_layout_rtl() ^ (p_point.x < get_tab_rect(0).position.x)) ? 0 : get_tab_count();
1161 }
1162
1163 Tab moving_tab = from_tabs->tabs[tab_from_id];
1164 from_tabs->remove_tab(tab_from_id);
1165 tabs.insert(hover_now, moving_tab);
1166
1167 if (tabs.size() > 1) {
1168 if (current >= hover_now) {
1169 current++;
1170 }
1171 if (previous >= hover_now) {
1172 previous++;
1173 }
1174 }
1175
1176 if (!is_tab_disabled(hover_now)) {
1177 set_current_tab(hover_now);
1178 } else {
1179 _update_cache();
1180 queue_redraw();
1181 }
1182
1183 update_minimum_size();
1184
1185 if (tabs.size() == 1) {
1186 emit_signal(SNAME("tab_selected"), 0);
1187 emit_signal(SNAME("tab_changed"), 0);
1188 }
1189 }
1190 }
1191 }
1192}
1193
1194int TabBar::get_tab_idx_at_point(const Point2 &p_point) const {
1195 int hover_now = -1;
1196
1197 if (!tabs.is_empty()) {
1198 for (int i = offset; i <= max_drawn_tab; i++) {
1199 Rect2 rect = get_tab_rect(i);
1200 if (rect.has_point(p_point)) {
1201 hover_now = i;
1202 }
1203 }
1204 }
1205
1206 return hover_now;
1207}
1208
1209void TabBar::set_tab_alignment(AlignmentMode p_alignment) {
1210 ERR_FAIL_INDEX(p_alignment, ALIGNMENT_MAX);
1211
1212 if (tab_alignment == p_alignment) {
1213 return;
1214 }
1215
1216 tab_alignment = p_alignment;
1217
1218 _update_cache();
1219 queue_redraw();
1220}
1221
1222TabBar::AlignmentMode TabBar::get_tab_alignment() const {
1223 return tab_alignment;
1224}
1225
1226void TabBar::set_clip_tabs(bool p_clip_tabs) {
1227 if (clip_tabs == p_clip_tabs) {
1228 return;
1229 }
1230 clip_tabs = p_clip_tabs;
1231
1232 if (!clip_tabs) {
1233 offset = 0;
1234 max_drawn_tab = 0;
1235 }
1236
1237 _update_cache();
1238 if (scroll_to_selected) {
1239 ensure_tab_visible(current);
1240 }
1241 queue_redraw();
1242 update_minimum_size();
1243}
1244
1245bool TabBar::get_clip_tabs() const {
1246 return clip_tabs;
1247}
1248
1249void TabBar::move_tab(int p_from, int p_to) {
1250 if (p_from == p_to) {
1251 return;
1252 }
1253
1254 ERR_FAIL_INDEX(p_from, tabs.size());
1255 ERR_FAIL_INDEX(p_to, tabs.size());
1256
1257 Tab tab_from = tabs[p_from];
1258 tabs.remove_at(p_from);
1259 tabs.insert(p_to, tab_from);
1260
1261 if (current == p_from) {
1262 current = p_to;
1263 } else if (current > p_from && current <= p_to) {
1264 current--;
1265 } else if (current < p_from && current >= p_to) {
1266 current++;
1267 }
1268
1269 if (previous == p_from) {
1270 previous = p_to;
1271 } else if (previous > p_from && previous >= p_to) {
1272 previous--;
1273 } else if (previous < p_from && previous <= p_to) {
1274 previous++;
1275 }
1276
1277 _update_cache();
1278 _ensure_no_over_offset();
1279 if (scroll_to_selected) {
1280 ensure_tab_visible(current);
1281 }
1282 queue_redraw();
1283 notify_property_list_changed();
1284}
1285
1286int TabBar::get_tab_width(int p_idx) const {
1287 ERR_FAIL_INDEX_V(p_idx, tabs.size(), 0);
1288
1289 Ref<StyleBox> style;
1290
1291 if (tabs[p_idx].disabled) {
1292 style = theme_cache.tab_disabled_style;
1293 } else if (current == p_idx) {
1294 style = theme_cache.tab_selected_style;
1295 } else if (hover == p_idx) {
1296 style = theme_cache.tab_hovered_style;
1297 } else {
1298 style = theme_cache.tab_unselected_style;
1299 }
1300 int x = style->get_minimum_size().width;
1301
1302 if (tabs[p_idx].icon.is_valid()) {
1303 const Size2 icon_size = _get_tab_icon_size(p_idx);
1304 x += icon_size.width + theme_cache.h_separation;
1305 }
1306
1307 if (!tabs[p_idx].text.is_empty()) {
1308 x += tabs[p_idx].size_text + theme_cache.h_separation;
1309 }
1310
1311 bool close_visible = cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && p_idx == current);
1312
1313 if (tabs[p_idx].right_button.is_valid()) {
1314 Ref<StyleBox> btn_style = theme_cache.button_hl_style;
1315 Ref<Texture2D> rb = tabs[p_idx].right_button;
1316
1317 if (close_visible) {
1318 x += btn_style->get_minimum_size().width + rb->get_width();
1319 } else {
1320 x += btn_style->get_margin(SIDE_LEFT) + rb->get_width() + theme_cache.h_separation;
1321 }
1322 }
1323
1324 if (close_visible) {
1325 Ref<StyleBox> btn_style = theme_cache.button_hl_style;
1326 Ref<Texture2D> cb = theme_cache.close_icon;
1327 x += btn_style->get_margin(SIDE_LEFT) + cb->get_width() + theme_cache.h_separation;
1328 }
1329
1330 if (x > style->get_minimum_size().width) {
1331 x -= theme_cache.h_separation;
1332 }
1333
1334 return x;
1335}
1336
1337Size2 TabBar::_get_tab_icon_size(int p_index) const {
1338 ERR_FAIL_INDEX_V(p_index, tabs.size(), Size2());
1339 const TabBar::Tab &tab = tabs[p_index];
1340 Size2 icon_size = tab.icon->get_size();
1341
1342 int icon_max_width = 0;
1343 if (theme_cache.icon_max_width > 0) {
1344 icon_max_width = theme_cache.icon_max_width;
1345 }
1346 if (tab.icon_max_width > 0 && (icon_max_width == 0 || tab.icon_max_width < icon_max_width)) {
1347 icon_max_width = tab.icon_max_width;
1348 }
1349
1350 if (icon_max_width > 0 && icon_size.width > icon_max_width) {
1351 icon_size.height = icon_size.height * icon_max_width / icon_size.width;
1352 icon_size.width = icon_max_width;
1353 }
1354
1355 return icon_size;
1356}
1357
1358void TabBar::_ensure_no_over_offset() {
1359 if (!is_inside_tree() || !buttons_visible) {
1360 return;
1361 }
1362
1363 int limit_minus_buttons = get_size().width - theme_cache.increment_icon->get_width() - theme_cache.decrement_icon->get_width();
1364
1365 int prev_offset = offset;
1366
1367 int total_w = tabs[max_drawn_tab].ofs_cache + tabs[max_drawn_tab].size_cache - tabs[offset].ofs_cache;
1368 for (int i = offset; i > 0; i--) {
1369 if (tabs[i - 1].hidden) {
1370 continue;
1371 }
1372
1373 total_w += tabs[i - 1].size_cache;
1374
1375 if (total_w < limit_minus_buttons) {
1376 offset--;
1377 } else {
1378 break;
1379 }
1380 }
1381
1382 if (prev_offset != offset) {
1383 _update_cache();
1384 queue_redraw();
1385 }
1386}
1387
1388void TabBar::ensure_tab_visible(int p_idx) {
1389 if (!is_inside_tree() || !buttons_visible) {
1390 return;
1391 }
1392 ERR_FAIL_INDEX(p_idx, tabs.size());
1393
1394 if (tabs[p_idx].hidden || (p_idx >= offset && p_idx <= max_drawn_tab)) {
1395 return;
1396 }
1397
1398 if (p_idx < offset) {
1399 offset = p_idx;
1400 _update_cache();
1401 queue_redraw();
1402
1403 return;
1404 }
1405
1406 int limit_minus_buttons = get_size().width - theme_cache.increment_icon->get_width() - theme_cache.decrement_icon->get_width();
1407
1408 int total_w = tabs[max_drawn_tab].ofs_cache - tabs[offset].ofs_cache;
1409 for (int i = max_drawn_tab; i <= p_idx; i++) {
1410 if (tabs[i].hidden) {
1411 continue;
1412 }
1413
1414 total_w += tabs[i].size_cache;
1415 }
1416
1417 int prev_offset = offset;
1418
1419 for (int i = offset; i < p_idx; i++) {
1420 if (tabs[i].hidden) {
1421 continue;
1422 }
1423
1424 if (total_w > limit_minus_buttons) {
1425 total_w -= tabs[i].size_cache;
1426 offset++;
1427 } else {
1428 break;
1429 }
1430 }
1431
1432 if (prev_offset != offset) {
1433 _update_cache();
1434 queue_redraw();
1435 }
1436}
1437
1438Rect2 TabBar::get_tab_rect(int p_tab) const {
1439 ERR_FAIL_INDEX_V(p_tab, tabs.size(), Rect2());
1440 if (is_layout_rtl()) {
1441 return Rect2(get_size().width - tabs[p_tab].ofs_cache - tabs[p_tab].size_cache, 0, tabs[p_tab].size_cache, get_size().height);
1442 } else {
1443 return Rect2(tabs[p_tab].ofs_cache, 0, tabs[p_tab].size_cache, get_size().height);
1444 }
1445}
1446
1447void TabBar::set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy) {
1448 ERR_FAIL_INDEX(p_policy, CLOSE_BUTTON_MAX);
1449
1450 if (cb_displaypolicy == p_policy) {
1451 return;
1452 }
1453
1454 cb_displaypolicy = p_policy;
1455
1456 _update_cache();
1457 _ensure_no_over_offset();
1458 if (scroll_to_selected) {
1459 ensure_tab_visible(current);
1460 }
1461 queue_redraw();
1462 update_minimum_size();
1463}
1464
1465TabBar::CloseButtonDisplayPolicy TabBar::get_tab_close_display_policy() const {
1466 return cb_displaypolicy;
1467}
1468
1469void TabBar::set_max_tab_width(int p_width) {
1470 ERR_FAIL_COND(p_width < 0);
1471
1472 if (max_width == p_width) {
1473 return;
1474 }
1475
1476 max_width = p_width;
1477
1478 _update_cache();
1479 _ensure_no_over_offset();
1480 if (scroll_to_selected) {
1481 ensure_tab_visible(current);
1482 }
1483 queue_redraw();
1484 update_minimum_size();
1485}
1486
1487int TabBar::get_max_tab_width() const {
1488 return max_width;
1489}
1490
1491void TabBar::set_scrolling_enabled(bool p_enabled) {
1492 scrolling_enabled = p_enabled;
1493}
1494
1495bool TabBar::get_scrolling_enabled() const {
1496 return scrolling_enabled;
1497}
1498
1499void TabBar::set_drag_to_rearrange_enabled(bool p_enabled) {
1500 drag_to_rearrange_enabled = p_enabled;
1501}
1502
1503bool TabBar::get_drag_to_rearrange_enabled() const {
1504 return drag_to_rearrange_enabled;
1505}
1506
1507void TabBar::set_tabs_rearrange_group(int p_group_id) {
1508 tabs_rearrange_group = p_group_id;
1509}
1510
1511int TabBar::get_tabs_rearrange_group() const {
1512 return tabs_rearrange_group;
1513}
1514
1515void TabBar::set_scroll_to_selected(bool p_enabled) {
1516 scroll_to_selected = p_enabled;
1517 if (p_enabled) {
1518 ensure_tab_visible(current);
1519 }
1520}
1521
1522bool TabBar::get_scroll_to_selected() const {
1523 return scroll_to_selected;
1524}
1525
1526void TabBar::set_select_with_rmb(bool p_enabled) {
1527 select_with_rmb = p_enabled;
1528}
1529
1530bool TabBar::get_select_with_rmb() const {
1531 return select_with_rmb;
1532}
1533
1534bool TabBar::_set(const StringName &p_name, const Variant &p_value) {
1535 Vector<String> components = String(p_name).split("/", true, 2);
1536 if (components.size() >= 2 && components[0].begins_with("tab_") && components[0].trim_prefix("tab_").is_valid_int()) {
1537 int tab_index = components[0].trim_prefix("tab_").to_int();
1538 String property = components[1];
1539 if (property == "title") {
1540 set_tab_title(tab_index, p_value);
1541 return true;
1542 } else if (property == "icon") {
1543 set_tab_icon(tab_index, p_value);
1544 return true;
1545 } else if (components[1] == "disabled") {
1546 set_tab_disabled(tab_index, p_value);
1547 return true;
1548 }
1549 }
1550 return false;
1551}
1552
1553bool TabBar::_get(const StringName &p_name, Variant &r_ret) const {
1554 Vector<String> components = String(p_name).split("/", true, 2);
1555 if (components.size() >= 2 && components[0].begins_with("tab_") && components[0].trim_prefix("tab_").is_valid_int()) {
1556 int tab_index = components[0].trim_prefix("tab_").to_int();
1557 String property = components[1];
1558 if (property == "title") {
1559 r_ret = get_tab_title(tab_index);
1560 return true;
1561 } else if (property == "icon") {
1562 r_ret = get_tab_icon(tab_index);
1563 return true;
1564 } else if (components[1] == "disabled") {
1565 r_ret = is_tab_disabled(tab_index);
1566 return true;
1567 }
1568 }
1569 return false;
1570}
1571
1572void TabBar::_get_property_list(List<PropertyInfo> *p_list) const {
1573 for (int i = 0; i < tabs.size(); i++) {
1574 p_list->push_back(PropertyInfo(Variant::STRING, vformat("tab_%d/title", i)));
1575
1576 PropertyInfo pi = PropertyInfo(Variant::OBJECT, vformat("tab_%d/icon", i), PROPERTY_HINT_RESOURCE_TYPE, "Texture2D");
1577 pi.usage &= ~(get_tab_icon(i).is_null() ? PROPERTY_USAGE_STORAGE : 0);
1578 p_list->push_back(pi);
1579
1580 pi = PropertyInfo(Variant::BOOL, vformat("tab_%d/disabled", i));
1581 pi.usage &= ~(!is_tab_disabled(i) ? PROPERTY_USAGE_STORAGE : 0);
1582 p_list->push_back(pi);
1583 }
1584}
1585
1586void TabBar::_bind_methods() {
1587 ClassDB::bind_method(D_METHOD("set_tab_count", "count"), &TabBar::set_tab_count);
1588 ClassDB::bind_method(D_METHOD("get_tab_count"), &TabBar::get_tab_count);
1589 ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &TabBar::set_current_tab);
1590 ClassDB::bind_method(D_METHOD("get_current_tab"), &TabBar::get_current_tab);
1591 ClassDB::bind_method(D_METHOD("get_previous_tab"), &TabBar::get_previous_tab);
1592 ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &TabBar::set_tab_title);
1593 ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &TabBar::get_tab_title);
1594 ClassDB::bind_method(D_METHOD("set_tab_text_direction", "tab_idx", "direction"), &TabBar::set_tab_text_direction);
1595 ClassDB::bind_method(D_METHOD("get_tab_text_direction", "tab_idx"), &TabBar::get_tab_text_direction);
1596 ClassDB::bind_method(D_METHOD("set_tab_language", "tab_idx", "language"), &TabBar::set_tab_language);
1597 ClassDB::bind_method(D_METHOD("get_tab_language", "tab_idx"), &TabBar::get_tab_language);
1598 ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabBar::set_tab_icon);
1599 ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabBar::get_tab_icon);
1600 ClassDB::bind_method(D_METHOD("set_tab_icon_max_width", "tab_idx", "width"), &TabBar::set_tab_icon_max_width);
1601 ClassDB::bind_method(D_METHOD("get_tab_icon_max_width", "tab_idx"), &TabBar::get_tab_icon_max_width);
1602 ClassDB::bind_method(D_METHOD("set_tab_button_icon", "tab_idx", "icon"), &TabBar::set_tab_button_icon);
1603 ClassDB::bind_method(D_METHOD("get_tab_button_icon", "tab_idx"), &TabBar::get_tab_button_icon);
1604 ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabBar::set_tab_disabled);
1605 ClassDB::bind_method(D_METHOD("is_tab_disabled", "tab_idx"), &TabBar::is_tab_disabled);
1606 ClassDB::bind_method(D_METHOD("set_tab_hidden", "tab_idx", "hidden"), &TabBar::set_tab_hidden);
1607 ClassDB::bind_method(D_METHOD("is_tab_hidden", "tab_idx"), &TabBar::is_tab_hidden);
1608 ClassDB::bind_method(D_METHOD("set_tab_metadata", "tab_idx", "metadata"), &TabBar::set_tab_metadata);
1609 ClassDB::bind_method(D_METHOD("get_tab_metadata", "tab_idx"), &TabBar::get_tab_metadata);
1610 ClassDB::bind_method(D_METHOD("remove_tab", "tab_idx"), &TabBar::remove_tab);
1611 ClassDB::bind_method(D_METHOD("add_tab", "title", "icon"), &TabBar::add_tab, DEFVAL(""), DEFVAL(Ref<Texture2D>()));
1612 ClassDB::bind_method(D_METHOD("get_tab_idx_at_point", "point"), &TabBar::get_tab_idx_at_point);
1613 ClassDB::bind_method(D_METHOD("set_tab_alignment", "alignment"), &TabBar::set_tab_alignment);
1614 ClassDB::bind_method(D_METHOD("get_tab_alignment"), &TabBar::get_tab_alignment);
1615 ClassDB::bind_method(D_METHOD("set_clip_tabs", "clip_tabs"), &TabBar::set_clip_tabs);
1616 ClassDB::bind_method(D_METHOD("get_clip_tabs"), &TabBar::get_clip_tabs);
1617 ClassDB::bind_method(D_METHOD("get_tab_offset"), &TabBar::get_tab_offset);
1618 ClassDB::bind_method(D_METHOD("get_offset_buttons_visible"), &TabBar::get_offset_buttons_visible);
1619 ClassDB::bind_method(D_METHOD("ensure_tab_visible", "idx"), &TabBar::ensure_tab_visible);
1620 ClassDB::bind_method(D_METHOD("get_tab_rect", "tab_idx"), &TabBar::get_tab_rect);
1621 ClassDB::bind_method(D_METHOD("move_tab", "from", "to"), &TabBar::move_tab);
1622 ClassDB::bind_method(D_METHOD("set_tab_close_display_policy", "policy"), &TabBar::set_tab_close_display_policy);
1623 ClassDB::bind_method(D_METHOD("get_tab_close_display_policy"), &TabBar::get_tab_close_display_policy);
1624 ClassDB::bind_method(D_METHOD("set_max_tab_width", "width"), &TabBar::set_max_tab_width);
1625 ClassDB::bind_method(D_METHOD("get_max_tab_width"), &TabBar::get_max_tab_width);
1626 ClassDB::bind_method(D_METHOD("set_scrolling_enabled", "enabled"), &TabBar::set_scrolling_enabled);
1627 ClassDB::bind_method(D_METHOD("get_scrolling_enabled"), &TabBar::get_scrolling_enabled);
1628 ClassDB::bind_method(D_METHOD("set_drag_to_rearrange_enabled", "enabled"), &TabBar::set_drag_to_rearrange_enabled);
1629 ClassDB::bind_method(D_METHOD("get_drag_to_rearrange_enabled"), &TabBar::get_drag_to_rearrange_enabled);
1630 ClassDB::bind_method(D_METHOD("set_tabs_rearrange_group", "group_id"), &TabBar::set_tabs_rearrange_group);
1631 ClassDB::bind_method(D_METHOD("get_tabs_rearrange_group"), &TabBar::get_tabs_rearrange_group);
1632 ClassDB::bind_method(D_METHOD("set_scroll_to_selected", "enabled"), &TabBar::set_scroll_to_selected);
1633 ClassDB::bind_method(D_METHOD("get_scroll_to_selected"), &TabBar::get_scroll_to_selected);
1634 ClassDB::bind_method(D_METHOD("set_select_with_rmb", "enabled"), &TabBar::set_select_with_rmb);
1635 ClassDB::bind_method(D_METHOD("get_select_with_rmb"), &TabBar::get_select_with_rmb);
1636 ClassDB::bind_method(D_METHOD("clear_tabs"), &TabBar::clear_tabs);
1637
1638 ADD_SIGNAL(MethodInfo("tab_selected", PropertyInfo(Variant::INT, "tab")));
1639 ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab")));
1640 ADD_SIGNAL(MethodInfo("tab_clicked", PropertyInfo(Variant::INT, "tab")));
1641 ADD_SIGNAL(MethodInfo("tab_rmb_clicked", PropertyInfo(Variant::INT, "tab")));
1642 ADD_SIGNAL(MethodInfo("tab_close_pressed", PropertyInfo(Variant::INT, "tab")));
1643 ADD_SIGNAL(MethodInfo("tab_button_pressed", PropertyInfo(Variant::INT, "tab")));
1644 ADD_SIGNAL(MethodInfo("tab_hovered", PropertyInfo(Variant::INT, "tab")));
1645 ADD_SIGNAL(MethodInfo("active_tab_rearranged", PropertyInfo(Variant::INT, "idx_to")));
1646
1647 ADD_PROPERTY(PropertyInfo(Variant::INT, "current_tab", PROPERTY_HINT_RANGE, "-1,4096,1", PROPERTY_USAGE_EDITOR), "set_current_tab", "get_current_tab");
1648 ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_alignment", "get_tab_alignment");
1649 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_tabs"), "set_clip_tabs", "get_clip_tabs");
1650 ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_close_display_policy", PROPERTY_HINT_ENUM, "Show Never,Show Active Only,Show Always"), "set_tab_close_display_policy", "get_tab_close_display_policy");
1651 ADD_PROPERTY(PropertyInfo(Variant::INT, "max_tab_width", PROPERTY_HINT_RANGE, "0,99999,1,suffix:px"), "set_max_tab_width", "get_max_tab_width");
1652 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scrolling_enabled"), "set_scrolling_enabled", "get_scrolling_enabled");
1653 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled");
1654 ADD_PROPERTY(PropertyInfo(Variant::INT, "tabs_rearrange_group"), "set_tabs_rearrange_group", "get_tabs_rearrange_group");
1655 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_to_selected"), "set_scroll_to_selected", "get_scroll_to_selected");
1656 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "select_with_rmb"), "set_select_with_rmb", "get_select_with_rmb");
1657
1658 ADD_ARRAY_COUNT("Tabs", "tab_count", "set_tab_count", "get_tab_count", "tab_");
1659
1660 BIND_ENUM_CONSTANT(ALIGNMENT_LEFT);
1661 BIND_ENUM_CONSTANT(ALIGNMENT_CENTER);
1662 BIND_ENUM_CONSTANT(ALIGNMENT_RIGHT);
1663 BIND_ENUM_CONSTANT(ALIGNMENT_MAX);
1664
1665 BIND_ENUM_CONSTANT(CLOSE_BUTTON_SHOW_NEVER);
1666 BIND_ENUM_CONSTANT(CLOSE_BUTTON_SHOW_ACTIVE_ONLY);
1667 BIND_ENUM_CONSTANT(CLOSE_BUTTON_SHOW_ALWAYS);
1668 BIND_ENUM_CONSTANT(CLOSE_BUTTON_MAX);
1669
1670 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabBar, h_separation);
1671 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabBar, icon_max_width);
1672
1673 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_unselected_style, "tab_unselected");
1674 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_hovered_style, "tab_hovered");
1675 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_selected_style, "tab_selected");
1676 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_disabled_style, "tab_disabled");
1677
1678 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, increment_icon, "increment");
1679 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, increment_hl_icon, "increment_highlight");
1680 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, decrement_icon, "decrement");
1681 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, decrement_hl_icon, "decrement_highlight");
1682 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, drop_mark_icon, "drop_mark");
1683 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabBar, drop_mark_color);
1684
1685 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabBar, font_selected_color);
1686 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabBar, font_hovered_color);
1687 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabBar, font_unselected_color);
1688 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabBar, font_disabled_color);
1689 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabBar, font_outline_color);
1690
1691 BIND_THEME_ITEM(Theme::DATA_TYPE_FONT, TabBar, font);
1692 BIND_THEME_ITEM(Theme::DATA_TYPE_FONT_SIZE, TabBar, font_size);
1693 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabBar, outline_size);
1694
1695 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, close_icon, "close");
1696 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, button_pressed_style, "button_pressed");
1697 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, button_hl_style, "button_highlight");
1698}
1699
1700TabBar::TabBar() {
1701 set_size(Size2(get_size().width, get_minimum_size().height));
1702 connect("mouse_exited", callable_mp(this, &TabBar::_on_mouse_exited));
1703}
1704