1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/tabs.h"
13
14#include "app/color_utils.h"
15#include "app/modules/gfx.h"
16#include "app/modules/gui.h"
17#include "app/ui/editor/editor_view.h"
18#include "app/ui/skin/skin_theme.h"
19#include "os/font.h"
20#include "os/surface.h"
21#include "os/system.h"
22#include "ui/intern.h"
23#include "ui/ui.h"
24
25#include <algorithm>
26#include <cmath>
27
28#define ANI_ADDING_TAB_TICKS 5
29#define ANI_REMOVING_TAB_TICKS 10
30#define ANI_REORDER_TABS_TICKS 5
31
32#define HAS_ARROWS(tabs) ((m_button_left->parent() == (tabs)))
33
34namespace app {
35
36using namespace app::skin;
37using namespace ui;
38
39WidgetType Tabs::Type()
40{
41 static WidgetType type = kGenericWidget;
42 if (type == kGenericWidget)
43 type = register_widget_type();
44 return type;
45}
46
47Tabs::Tabs(TabsDelegate* delegate)
48 : Widget(Tabs::Type())
49 , m_border(2)
50 , m_docked(false)
51 , m_hot(nullptr)
52 , m_hotCloseButton(false)
53 , m_clickedCloseButton(false)
54 , m_selected(nullptr)
55 , m_delegate(delegate)
56 , m_addedTab(nullptr)
57 , m_removedTab(nullptr)
58 , m_isDragging(false)
59 , m_dragCopy(false)
60 , m_dragTab(nullptr)
61 , m_floatingTab(nullptr)
62 , m_floatingOverlay(nullptr)
63 , m_dropNewTab(nullptr)
64 , m_dropNewIndex(-1)
65{
66 enableFlags(CTRL_RIGHT_CLICK);
67 setDoubleBuffered(true);
68 initTheme();
69}
70
71Tabs::~Tabs()
72{
73 m_addedTab.reset();
74 m_removedTab.reset();
75
76 // Stop animation
77 stopAnimation();
78
79 // Remove all tabs
80 m_list.clear();
81}
82
83void Tabs::addTab(TabView* tabView, bool from_drop, int pos)
84{
85 resetOldPositions();
86 if (!from_drop || m_list.empty())
87 startAnimation(ANI_ADDING_TAB, ANI_ADDING_TAB_TICKS);
88 else
89 startAnimation(ANI_REORDER_TABS, ANI_REORDER_TABS_TICKS);
90
91 TabPtr tab(new Tab(tabView));
92 if (pos < 0)
93 m_list.push_back(tab);
94 else
95 m_list.insert(m_list.begin()+pos, tab);
96
97 updateTabs();
98
99 tab->oldX = (from_drop ? m_dropNewPosX-tab->width/2: tab->x);
100 if (tab->oldX < 0)
101 tab->oldX = 0;
102
103 tab->oldWidth = tab->width;
104 tab->modified = (m_delegate ? m_delegate->isTabModified(this, tabView): false);
105
106 m_addedTab = tab;
107}
108
109void Tabs::removeTab(TabView* tabView, bool with_animation)
110{
111 TabPtr tab(getTabByView(tabView));
112 if (!tab)
113 return;
114
115 if (m_hot == tab)
116 m_hot.reset();
117
118 if (m_selected == tab) {
119 if (tab == m_list.back())
120 selectPreviousTab();
121 else
122 selectNextTab();
123
124 if (m_selected == tab)
125 m_selected.reset();
126 }
127
128 if (with_animation)
129 resetOldPositions();
130
131 TabsListIterator it =
132 std::find(m_list.begin(), m_list.end(), tab);
133 ASSERT(it != m_list.end() && "Removing a tab that is not part of the Tabs widget");
134 it = m_list.erase(it);
135
136 m_removedTab = tab;
137
138 if (with_animation) {
139 if (m_delegate)
140 tab->modified = m_delegate->isTabModified(this, tabView);
141 tab->view = nullptr; // The view will be destroyed after Tabs::removeTab() anyway
142
143 startAnimation(ANI_REMOVING_TAB, ANI_REMOVING_TAB_TICKS);
144 }
145
146 updateTabs();
147}
148
149void Tabs::updateTabs()
150{
151 auto theme = SkinTheme::get(this);
152 double availWidth = bounds().w - m_border*ui::guiscale();
153 double defTabWidth = theme->dimensions.tabsWidth();
154 double tabWidth = defTabWidth;
155 if (tabWidth * m_list.size() > availWidth) {
156 tabWidth = availWidth / double(m_list.size());
157 tabWidth = std::max(double(4*ui::guiscale()), tabWidth);
158 }
159 double x = 0.0;
160 int i = 0;
161
162 for (auto& tab : m_list) {
163 if (tab == m_floatingTab && !m_dragCopy) {
164 ++i;
165 continue;
166 }
167
168 if ((m_dropNewTab && m_dropNewIndex == i) ||
169 (m_dragTab && !m_floatingTab &&
170 m_dragCopy &&
171 m_dragCopyIndex == i)) {
172 x += tabWidth;
173 }
174
175 tab->text = tab->view->getTabText();
176 tab->icon = tab->view->getTabIcon();
177 tab->color = tab->view->getTabColor();
178 tab->x = int(x);
179 tab->width = int(x+tabWidth) - int(x);
180 x += tabWidth;
181 ++i;
182 }
183
184 calculateHot();
185 invalidate();
186}
187
188// Returns true if the user can select other tab.
189bool Tabs::canSelectOtherTab() const
190{
191 return (m_selected && !m_isDragging);
192}
193
194void Tabs::selectTab(TabView* tabView)
195{
196 ASSERT(tabView != NULL);
197
198 TabPtr tab(getTabByView(tabView));
199 if (tab)
200 selectTabInternal(tab);
201}
202
203void Tabs::selectNextTab()
204{
205 if (!m_selected)
206 return;
207
208 TabsListIterator currentTabIt = getTabIteratorByView(m_selected->view);
209 TabsListIterator it = currentTabIt;
210 if (it != m_list.end()) {
211 // If we are at the end of the list, cycle to the first tab.
212 if (it == --m_list.end())
213 it = m_list.begin();
214 // Go to next tab.
215 else
216 ++it;
217
218 if (it != currentTabIt)
219 selectTabInternal(*it);
220 }
221}
222
223void Tabs::selectPreviousTab()
224{
225 if (!m_selected)
226 return;
227
228 TabsListIterator currentTabIt = getTabIteratorByView(m_selected->view);
229 TabsListIterator it = currentTabIt;
230 if (it != m_list.end()) {
231 // If we are at the beginning of the list, cycle to the last tab.
232 if (it == m_list.begin())
233 it = --m_list.end();
234 // Go to previous tab.
235 else
236 --it;
237
238 if (it != currentTabIt)
239 selectTabInternal(*it);
240 }
241}
242
243TabView* Tabs::getSelectedTab()
244{
245 if (m_selected)
246 return m_selected->view;
247 else
248 return NULL;
249}
250
251void Tabs::setDockedStyle()
252{
253 m_docked = true;
254 initTheme();
255}
256
257void Tabs::setDropViewPreview(const gfx::Point& screenPos,
258 TabView* view)
259{
260 int x0 = (display()->nativeWindow()->pointFromScreen(screenPos).x - bounds().x);
261 int newIndex = -1;
262
263 if (!m_list.empty()) {
264 newIndex = x0 / m_list[0]->width;
265 newIndex = std::clamp(newIndex, 0, (int)m_list.size());
266 }
267 else
268 newIndex = 0;
269
270 bool startAni = (m_dropNewIndex != newIndex ||
271 m_dropNewTab != view);
272
273 m_dropNewIndex = newIndex;
274 m_dropNewPosX = x0;
275 m_dropNewTab = view;
276
277 if (startAni)
278 startReorderTabsAnimation();
279 else
280 invalidate();
281}
282
283void Tabs::removeDropViewPreview()
284{
285 m_dropNewTab = nullptr;
286
287 startReorderTabsAnimation();
288}
289
290bool Tabs::onProcessMessage(Message* msg)
291{
292 switch (msg->type()) {
293
294 case kMouseEnterMessage:
295 calculateHot();
296 break;
297
298 case kMouseMoveMessage:
299 calculateHot();
300 updateDragCopyCursor(msg);
301
302 if (hasCapture() && m_selected) {
303 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
304 const gfx::Point mousePos = mouseMsg->position();
305 const gfx::Point screenPos = mouseMsg->screenPosition();
306 const gfx::Point delta = mousePos - m_dragMousePos;
307
308 if (!m_isDragging) {
309 if (!m_clickedCloseButton && mouseMsg->left()) {
310 double dist = std::sqrt(delta.x*delta.x + delta.y*delta.y);
311 if (dist > 4.0/ui::guiscale())
312 startDrag();
313 }
314 }
315 // We are dragging a tab...
316 else {
317 // Floating tab (to create a new window)
318 if (!bounds().contains(mousePos) &&
319 (ABS(delta.y) > 16*guiscale() ||
320 mousePos.x < bounds().x-16*guiscale() ||
321 mousePos.x > bounds().x2()+16*guiscale())) {
322 DropViewPreviewResult result = DropViewPreviewResult::FLOATING;
323
324 if (!m_floatingTab) {
325 resetOldPositions();
326 m_floatingTab = m_selected;
327 startRemoveDragTabAnimation();
328 }
329
330 if (m_delegate)
331 result = m_delegate->onFloatingTab(
332 this, m_selected->view, screenPos);
333
334 if (result != DropViewPreviewResult::DROP_IN_TABS) {
335 if (!m_floatingOverlay)
336 createFloatingOverlay(m_selected.get());
337 m_floatingOverlay->moveOverlay(mousePos - m_floatingOffset);
338 }
339 else {
340 destroyFloatingOverlay();
341 }
342 }
343 else {
344 destroyFloatingTab();
345
346 if (m_delegate)
347 m_delegate->onDockingTab(this, m_selected->view);
348 }
349
350 // Docked tab
351 if (!m_floatingTab) {
352 m_dragTab->oldX = m_dragTab->x = m_dragTabX + delta.x;
353 updateDragTabIndexes(mousePos.x, false);
354 }
355
356 invalidate();
357 }
358 }
359 return true;
360
361 case kMouseLeaveMessage:
362 if (m_hot) {
363 m_hot.reset();
364 if (m_delegate)
365 m_delegate->onMouseLeaveTab();
366 invalidate();
367 }
368 return true;
369
370 case kMouseDownMessage:
371 if (m_hot && !hasCapture()) {
372 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
373 m_dragMousePos = mouseMsg->position();
374 m_floatingOffset = mouseMsg->position() -
375 (bounds().origin() + getTabBounds(m_hot.get()).origin());
376
377 if (m_hotCloseButton) {
378 if (!m_clickedCloseButton) {
379 m_clickedCloseButton = true;
380 invalidate();
381 }
382 }
383 else if (mouseMsg->left()) {
384 selectTabInternal(m_hot);
385 }
386
387 captureMouse();
388 }
389 return true;
390
391 case kMouseUpMessage:
392 if (hasCapture()) {
393 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
394
395 releaseMouse();
396
397 if (!m_isDragging) {
398 if ((mouseMsg->middle()) ||
399 (mouseMsg->left() && m_hotCloseButton && m_clickedCloseButton)) {
400 if (m_hot && m_delegate)
401 m_delegate->onCloseTab(this, m_hot->view);
402 }
403 else if (mouseMsg->right() && m_hot) {
404 if (m_delegate)
405 m_delegate->onContextMenuTab(this, m_hot->view);
406 }
407
408 if (m_clickedCloseButton) {
409 m_clickedCloseButton = false;
410 invalidate();
411 }
412 }
413 else {
414 DropTabResult result = DropTabResult::NOT_HANDLED;
415
416 if (m_delegate) {
417 ASSERT(m_selected);
418 result = m_delegate->onDropTab(
419 this, m_selected->view,
420 mouseMsg->screenPosition(), m_dragCopy);
421 }
422
423 stopDrag(result);
424 }
425 }
426 return true;
427
428 case kMouseWheelMessage:
429 if (!m_isDragging) {
430 int dz =
431 (static_cast<MouseMessage*>(msg)->wheelDelta().x +
432 static_cast<MouseMessage*>(msg)->wheelDelta().y);
433
434 auto it = std::find(m_list.begin(), m_list.end(), m_selected);
435 if (it != m_list.end()) {
436 int index = (it - m_list.begin());
437 int newIndex = index + dz;
438 newIndex = std::clamp(newIndex, 0, int(m_list.size())-1);
439 if (newIndex != index) {
440 selectTabInternal(m_list[newIndex]);
441 }
442 }
443 return true;
444 }
445 break;
446
447 case kKeyDownMessage:
448 case kKeyUpMessage:
449 updateDragCopyCursor(msg);
450 break;
451
452 case kSetCursorMessage:
453 updateMouseCursor();
454 return true;
455
456 case kDoubleClickMessage:
457 // When we double-click outside tabs (!m_hot), we trigger the
458 // double-click in tabs container to show the "New Sprite" dialog.
459 if (!m_hot && m_delegate)
460 m_delegate->onTabsContainerDoubleClicked(this);
461 return true;
462
463 }
464
465 return Widget::onProcessMessage(msg);
466}
467
468void Tabs::onInitTheme(ui::InitThemeEvent& ev)
469{
470 Widget::onInitTheme(ev);
471 auto theme = SkinTheme::get(this);
472
473 if (m_docked) {
474 m_tabsHeight = theme->dimensions.dockedTabsHeight();
475 m_tabsBottomHeight = 0;
476 setStyle(theme->styles.workspaceTabs());
477 }
478 else {
479 m_tabsHeight = theme->dimensions.tabsHeight();
480 m_tabsBottomHeight = theme->dimensions.tabsBottomHeight();
481 setStyle(theme->styles.mainTabs());
482 }
483}
484
485void Tabs::onPaint(PaintEvent& ev)
486{
487 auto theme = SkinTheme::get(this);
488 Graphics* g = ev.graphics();
489 gfx::Rect rect = clientBounds();
490 gfx::Rect box(rect.x, rect.y, rect.w,
491 m_tabsHeight - m_tabsBottomHeight);
492
493 theme->paintWidget(g, this, style(), rect);
494
495 if (!m_docked)
496 drawFiller(g, box);
497
498 // For each tab...
499 for (TabPtr& tab : m_list) {
500 if (tab == m_floatingTab && !m_dragCopy)
501 continue;
502
503 box = getTabBounds(tab.get());
504
505 // The m_dragTab is drawn after all other regular tabs.
506 if ((!m_dragTab) ||
507 (tab->view != m_dragTab->view) ||
508 (m_dragCopy)) {
509 int dy = 0;
510 if (animation() == ANI_ADDING_TAB && tab == m_addedTab) {
511 double t = animationTime();
512 dy = int(box.h - box.h * t);
513 }
514
515 drawTab(g, box, tab.get(), dy,
516 (tab == m_hot),
517 (tab == m_selected));
518 }
519
520 box.x = box.x2();
521 }
522
523 // Draw deleted tab
524 if (animation() == ANI_REMOVING_TAB && m_removedTab) {
525 m_removedTab->width = 0;
526 box = getTabBounds(m_removedTab.get());
527 drawTab(g, box, m_removedTab.get(), 0,
528 (m_removedTab == m_floatingTab),
529 (m_removedTab == m_floatingTab));
530 }
531
532 // Tab that is being dragged. It's drawn here so it appears at the
533 // front of all other tabs.
534 if (m_dragTab && !m_floatingTab) {
535 TabPtr tab(m_dragTab);
536 box = getTabBounds(tab.get());
537 drawTab(g, box, tab.get(), 0, true, true);
538 }
539
540 // New tab from other Tab that want to be dropped here.
541 if (m_dropNewTab) {
542 Tab newTab(m_dropNewTab);
543
544 newTab.width = newTab.oldWidth =
545 (!m_list.empty() ? m_list[0]->width:
546 theme->dimensions.tabsWidth());
547
548 newTab.x = newTab.oldX =
549 m_dropNewPosX - newTab.width/2;
550
551 box = getTabBounds(&newTab);
552 drawTab(g, box, &newTab, 0, true, true);
553 }
554}
555
556void Tabs::onResize(ResizeEvent& ev)
557{
558 setBoundsQuietly(ev.bounds());
559 updateTabs();
560}
561
562void Tabs::onSizeHint(SizeHintEvent& ev)
563{
564 ev.setSizeHint(gfx::Size(0, m_tabsHeight));
565}
566
567void Tabs::selectTabInternal(TabPtr& tab)
568{
569 if (m_selected != tab) {
570 m_selected = tab;
571 makeTabVisible(tab.get());
572 invalidate();
573 }
574
575 if (m_delegate && tab)
576 m_delegate->onSelectTab(this, tab->view);
577}
578
579void Tabs::drawTab(Graphics* g, const gfx::Rect& _box,
580 Tab* tab, int dy, bool hover, bool selected)
581{
582 gfx::Rect box = _box;
583 if (box.w < ui::guiscale()*8)
584 box.w = ui::guiscale()*8;
585
586 auto theme = SkinTheme::get(this);
587 int clipTextRightSide;
588
589 gfx::Rect closeBox = getTabCloseButtonBounds(tab, box);
590 if (closeBox.isEmpty())
591 clipTextRightSide = 4*ui::guiscale();
592 else {
593 closeBox.y += dy;
594 clipTextRightSide = closeBox.w;
595 }
596
597 // Tab without text
598 PaintWidgetPartInfo info;
599 info.styleFlags =
600 (selected ? ui::Style::Layer::kFocus: 0) |
601 (hover ? ui::Style::Layer::kMouse: 0);
602 theme->paintWidgetPart(
603 g, theme->styles.tab(),
604 gfx::Rect(box.x, box.y+dy, box.w, box.h),
605 info);
606
607 gfx::Color tabColor = tab->color;
608 gfx::Color textColor = gfx::ColorNone;
609 if (tabColor != gfx::ColorNone) {
610 textColor = color_utils::blackandwhite_neg(tabColor);
611 if (!selected) {
612 tabColor = gfx::seta(tabColor, gfx::geta(tabColor)*3/4);
613 textColor = gfx::seta(textColor, gfx::geta(textColor)*3/4);
614 }
615 }
616
617 {
618 IntersectClip clip(g, gfx::Rect(box.x, box.y+dy, box.w-clipTextRightSide, box.h));
619
620 // Tab icon
621 TabIcon icon = tab->icon;
622 int dx = 0;
623 switch (icon) {
624 case TabIcon::NONE:
625 break;
626 case TabIcon::HOME:
627 {
628 theme->paintWidgetPart(
629 g, theme->styles.tabHome(),
630 gfx::Rect(
631 box.x,
632 box.y+dy,
633 box.x-dx,
634 box.h),
635 info);
636 dx += theme->dimensions.tabsIconWidth();
637 }
638 break;
639 }
640
641 // Tab with text + clipping the close button
642 if (box.w > 8*ui::guiscale()) {
643 Style* stylePtr = theme->styles.tabText();
644 Style newStyle(nullptr);
645
646 info.text = &tab->text;
647 if (tabColor != gfx::ColorNone) {
648 // TODO replace these fillRect() with a new theme part (which
649 // should be painted with the specific user-defined color)
650 g->fillRect(tabColor, gfx::Rect(box.x+dx+2, box.y+dy+3, box.w-dx-2, box.h-3));
651 g->fillRect(tabColor, gfx::Rect(box.x+dx+3, box.y+dy+2, box.w-dx-3, 1));
652
653 newStyle = Style(*stylePtr);
654 for (auto& layer : newStyle.layers()) {
655 if (layer.type() == ui::Style::Layer::Type::kText ||
656 layer.type() == ui::Style::Layer::Type::kBackground) {
657 layer.setColor(textColor);
658 }
659 }
660 stylePtr = &newStyle;
661 }
662 theme->paintWidgetPart(
663 g, stylePtr,
664 gfx::Rect(box.x+dx, box.y+dy, box.w-dx, box.h),
665 info);
666 info.text = nullptr;
667 }
668 }
669
670 // Tab bottom part
671 if (!m_docked) {
672 theme->paintWidgetPart(
673 g, theme->styles.tabBottom(),
674 gfx::Rect(box.x, box.y2(), box.w, bounds().y2()-box.y2()),
675 info);
676 }
677
678 // Close button
679 if (!closeBox.isEmpty()) {
680 ui::Style* style = theme->styles.tabCloseIcon();
681
682 if (m_delegate) {
683 if (tab->view)
684 tab->modified = m_delegate->isTabModified(this, tab->view);
685
686 if (tab->modified &&
687 (!hover || !m_hotCloseButton)) {
688 style = theme->styles.tabModifiedIcon();
689 }
690 }
691
692 if (tabColor != gfx::ColorNone) {
693 g->fillRect(tabColor, gfx::Rect(closeBox.x, closeBox.y+3, closeBox.w-3, closeBox.h-3));
694 g->fillRect(tabColor, gfx::Rect(closeBox.x, closeBox.y+2, closeBox.w-4, 1));
695 }
696
697 info.styleFlags = 0;
698 if (selected)
699 info.styleFlags |= ui::Style::Layer::kFocus;
700 if (hover && m_hotCloseButton) {
701 info.styleFlags |= ui::Style::Layer::kMouse;
702 if (m_clickedCloseButton)
703 info.styleFlags |= ui::Style::Layer::kSelected;
704 }
705
706 theme->paintWidgetPart(g, style, closeBox, info);
707 }
708}
709
710void Tabs::drawFiller(ui::Graphics* g, const gfx::Rect& box)
711{
712 auto theme = SkinTheme::get(this);
713 gfx::Rect rect = clientBounds();
714
715 theme->paintWidgetPart(
716 g, theme->styles.tabFiller(),
717 gfx::Rect(box.x, box.y, rect.x2()-box.x, box.h),
718 PaintWidgetPartInfo());
719
720 theme->paintWidgetPart(
721 g, theme->styles.tabBottom(),
722 gfx::Rect(box.x, box.y2(), rect.x2()-box.x, rect.y2()-box.y2()),
723 PaintWidgetPartInfo());
724}
725
726Tabs::TabsListIterator Tabs::getTabIteratorByView(TabView* tabView)
727{
728 TabsListIterator it, end = m_list.end();
729
730 for (it = m_list.begin(); it != end; ++it) {
731 if ((*it)->view == tabView)
732 break;
733 }
734
735 return it;
736}
737
738Tabs::TabPtr Tabs::getTabByView(TabView* tabView)
739{
740 TabsListIterator it = getTabIteratorByView(tabView);
741 if (it != m_list.end())
742 return TabPtr(*it);
743 else
744 return TabPtr(nullptr);
745}
746
747void Tabs::makeTabVisible(Tab* thisTab)
748{
749 updateTabs();
750}
751
752void Tabs::calculateHot()
753{
754 if (m_isDragging)
755 return;
756
757 gfx::Rect rect = bounds();
758 gfx::Rect box(rect.x+m_border*guiscale(), rect.y, 0, rect.h-1);
759 gfx::Point mousePos = mousePosInDisplay();
760 TabPtr hot(nullptr);
761 bool hotCloseButton = false;
762
763 // For each tab
764 for (TabPtr& tab : m_list) {
765 if (tab == m_floatingTab)
766 continue;
767
768 box.w = tab->width;
769
770 if (box.contains(mousePos)) {
771 hot = tab;
772 hotCloseButton = getTabCloseButtonBounds(tab.get(), box).contains(mousePos);
773 break;
774 }
775
776 box.x += box.w;
777 }
778
779 if (m_hot != hot ||
780 m_hotCloseButton != hotCloseButton) {
781 m_hot = hot;
782 m_hotCloseButton = hotCloseButton;
783
784 if (m_delegate)
785 m_delegate->onMouseOverTab(this, m_hot ? m_hot->view: NULL);
786
787 invalidate();
788 }
789}
790
791gfx::Rect Tabs::getTabCloseButtonBounds(Tab* tab, const gfx::Rect& box)
792{
793 auto theme = SkinTheme::get(this);
794 int iconW = theme->dimensions.tabsCloseIconWidth();
795 int iconH = theme->dimensions.tabsCloseIconHeight();
796
797 if (box.w-iconW > 32*ui::guiscale() || tab == m_selected.get())
798 return gfx::Rect(box.x2()-iconW, box.y+box.h/2-iconH/2, iconW, iconH);
799 else
800 return gfx::Rect();
801}
802
803void Tabs::resetOldPositions()
804{
805 for (TabPtr& tab : m_list) {
806 if (tab == m_floatingTab)
807 continue;
808
809 tab->oldX = tab->x;
810 tab->oldWidth = tab->width;
811 }
812}
813
814void Tabs::resetOldPositions(double t)
815{
816 for (TabPtr& tab : m_list) {
817 if (tab == m_floatingTab)
818 continue;
819
820 ASSERT(tab->x != (int)0xfefefefe);
821 ASSERT(tab->width != (int)0xfefefefe);
822 ASSERT(tab->oldX != (int)0xfefefefe);
823 ASSERT(tab->oldWidth != (int)0xfefefefe);
824
825 tab->oldX = int(inbetween(tab->oldX, tab->x, t));
826 tab->oldWidth = int(inbetween(tab->oldWidth, tab->width, t));
827 }
828}
829
830void Tabs::onAnimationFrame()
831{
832 invalidate();
833}
834
835void Tabs::onAnimationStop(int animation)
836{
837 m_addedTab.reset();
838
839 if (m_list.empty()) {
840 Widget* root = window();
841 if (root)
842 root->layout();
843 }
844}
845
846void Tabs::startDrag()
847{
848 ASSERT(m_selected);
849
850 updateTabs();
851
852 m_isDragging = true;
853 m_dragTab.reset(new Tab(m_selected->view));
854 m_dragTab->oldX = m_dragTab->x = m_dragTabX = m_selected->x;
855 m_dragTab->oldWidth = m_dragTab->width = m_selected->width;
856
857 m_dragTabIndex =
858 m_dragCopyIndex = std::find(m_list.begin(), m_list.end(), m_selected) - m_list.begin();
859
860 EditorView::SetScrollUpdateMethod(EditorView::KeepCenter);
861}
862
863void Tabs::stopDrag(DropTabResult result)
864{
865 m_isDragging = false;
866 ASSERT(m_dragTab);
867
868 switch (result) {
869
870 case DropTabResult::NOT_HANDLED:
871 case DropTabResult::DONT_REMOVE: {
872 destroyFloatingTab();
873
874 bool localCopy = false;
875 if (result == DropTabResult::NOT_HANDLED &&
876 m_dragTab && m_dragCopy && m_delegate) {
877 ASSERT(m_dragCopyIndex >= 0);
878
879 m_delegate->onCloneTab(this, m_dragTab->view, m_dragCopyIndex);
880
881 // To animate the new tab created by onCloneTab() from the
882 // m_dragTab position.
883 m_list[m_dragCopyIndex]->oldX = m_dragTab->x;
884 m_list[m_dragCopyIndex]->oldWidth = m_dragTab->width;
885 localCopy = true;
886 }
887
888 m_dragCopyIndex = -1;
889
890 startReorderTabsAnimation();
891
892 if (m_selected && m_dragTab && !localCopy) {
893 if (result == DropTabResult::NOT_HANDLED) {
894 // To animate m_selected tab from the m_dragTab position
895 // when we drop the tab in the same Tabs (with no copy)
896 m_selected->oldX = m_dragTab->x;
897 m_selected->oldWidth = m_dragTab->width;
898 }
899 else {
900 ASSERT(result == DropTabResult::DONT_REMOVE);
901
902 // In this case the tab was copied to other Tabs, so we
903 // avoid any kind of animation for the m_selected (it stays
904 // were it's).
905 m_selected->oldX = m_selected->x;
906 m_selected->oldWidth = m_selected->width;
907 }
908 }
909 break;
910 }
911
912 case DropTabResult::REMOVE:
913 m_floatingTab.reset();
914 m_removedTab.reset();
915 m_dragCopyIndex = -1;
916 destroyFloatingTab();
917
918 ASSERT(m_dragTab.get());
919 if (m_dragTab)
920 removeTab(m_dragTab->view, false);
921 break;
922
923 }
924
925 m_dragTab.reset();
926
927 EditorView::SetScrollUpdateMethod(EditorView::KeepOrigin);
928}
929
930gfx::Rect Tabs::getTabBounds(Tab* tab)
931{
932 gfx::Rect rect = clientBounds();
933 gfx::Rect box(rect.x, rect.y, rect.w, m_tabsHeight - m_tabsBottomHeight);
934 int startX = m_border*guiscale();
935 double t = animationTime();
936
937 if (animation() == ANI_NONE) {
938 box.x = startX + tab->x;
939 box.w = tab->width;
940 }
941 else {
942 ASSERT(tab->x != (int)0xfefefefe);
943 ASSERT(tab->width != (int)0xfefefefe);
944 ASSERT(tab->oldX != (int)0xfefefefe);
945 ASSERT(tab->oldWidth != (int)0xfefefefe);
946
947 box.x = startX + int(inbetween(tab->oldX, tab->x, t));
948 box.w = int(inbetween(tab->oldWidth, tab->width, t));
949 }
950
951 return box;
952}
953
954void Tabs::startReorderTabsAnimation()
955{
956 resetOldPositions(animationTime());
957 updateTabs();
958 startAnimation(ANI_REORDER_TABS, ANI_REORDER_TABS_TICKS);
959}
960
961void Tabs::startRemoveDragTabAnimation()
962{
963 m_removedTab.reset();
964 updateTabs();
965 startAnimation(ANI_REMOVING_TAB, ANI_REMOVING_TAB_TICKS);
966}
967
968void Tabs::createFloatingOverlay(Tab* tab)
969{
970 ASSERT(!m_floatingOverlay);
971
972 ui::Display* display = this->display();
973 os::SurfaceRef surface = os::instance()->makeRgbaSurface(
974 tab->width, m_tabsHeight);
975
976 // Fill the surface with pink color
977 {
978 os::SurfaceLock lock(surface.get());
979 os::Paint paint;
980 paint.color(gfx::rgba(0, 0, 0, 0));
981 paint.style(os::Paint::Fill);
982 surface->drawRect(gfx::Rect(0, 0, surface->width(), surface->height()), paint);
983 }
984 {
985 Graphics g(display, surface, 0, 0);
986 g.setFont(AddRef(font()));
987 drawTab(&g, g.getClipBounds(), tab, 0, true, true);
988 }
989
990 surface->setImmutable();
991
992 m_floatingOverlay = base::make_ref<ui::Overlay>(
993 display, surface, gfx::Point(),
994 (ui::Overlay::ZOrder)(Overlay::MouseZOrder-1));
995 OverlayManager::instance()->addOverlay(m_floatingOverlay);
996}
997
998void Tabs::destroyFloatingTab()
999{
1000 destroyFloatingOverlay();
1001
1002 if (m_floatingTab) {
1003 TabPtr tab(m_floatingTab);
1004 m_floatingTab.reset();
1005
1006 resetOldPositions();
1007 startAnimation(ANI_ADDING_TAB, ANI_ADDING_TAB_TICKS);
1008 updateTabs();
1009
1010 tab->oldX = tab->x;
1011 tab->oldWidth = 0;
1012
1013 m_addedTab = tab;
1014 }
1015}
1016
1017void Tabs::destroyFloatingOverlay()
1018{
1019 if (m_floatingOverlay) {
1020 OverlayManager::instance()->removeOverlay(m_floatingOverlay);
1021 m_floatingOverlay.reset();
1022 }
1023}
1024
1025void Tabs::updateMouseCursor()
1026{
1027 if (m_dragCopy)
1028 ui::set_mouse_cursor(kArrowPlusCursor);
1029 else
1030 ui::set_mouse_cursor(kArrowCursor);
1031}
1032
1033void Tabs::updateDragTabIndexes(int mouseX, bool startAni)
1034{
1035 if (m_dragTab) {
1036 int i = (mouseX - m_border*guiscale() - bounds().x) / m_dragTab->width;
1037
1038 if (m_dragCopy) {
1039 i = std::clamp(i, 0, int(m_list.size()));
1040 if (i != m_dragCopyIndex) {
1041 m_dragCopyIndex = i;
1042 startAni = true;
1043 }
1044 }
1045 else if (hasMouseOver()) {
1046 i = std::clamp(i, 0, int(m_list.size())-1);
1047 if (i != m_dragTabIndex) {
1048 m_list.erase(m_list.begin()+m_dragTabIndex);
1049 m_list.insert(m_list.begin()+i, m_selected);
1050 m_dragTabIndex = i;
1051 startAni = true;
1052 }
1053 }
1054 }
1055
1056 if (startAni)
1057 startReorderTabsAnimation();
1058}
1059
1060void Tabs::updateDragCopyCursor(ui::Message* msg)
1061{
1062 TabPtr tab = (m_isDragging ? m_dragTab: m_hot);
1063
1064 bool oldDragCopy = m_dragCopy;
1065 m_dragCopy = ((
1066#if !defined __APPLE__
1067 msg->ctrlPressed() ||
1068#endif
1069 msg->altPressed()) &&
1070 (tab && m_delegate && m_delegate->canCloneTab(this, tab->view)));
1071
1072 if (oldDragCopy != m_dragCopy) {
1073 updateDragTabIndexes(mousePosInDisplay().x, true);
1074 updateMouseCursor();
1075 }
1076}
1077
1078} // namespace app
1079