1// Aseprite UI Library
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "ui/window.h"
13
14#include "gfx/size.h"
15#include "ui/button.h"
16#include "ui/display.h"
17#include "ui/fit_bounds.h"
18#include "ui/graphics.h"
19#include "ui/intern.h"
20#include "ui/label.h"
21#include "ui/manager.h"
22#include "ui/message.h"
23#include "ui/message_loop.h"
24#include "ui/move_region.h"
25#include "ui/resize_event.h"
26#include "ui/scale.h"
27#include "ui/size_hint_event.h"
28#include "ui/system.h"
29#include "ui/theme.h"
30
31#include <algorithm>
32
33namespace ui {
34
35using namespace gfx;
36
37namespace {
38
39enum {
40 WINDOW_NONE = 0,
41 WINDOW_MOVE = 1,
42 WINDOW_RESIZE_LEFT = 2,
43 WINDOW_RESIZE_RIGHT = 4,
44 WINDOW_RESIZE_TOP = 8,
45 WINDOW_RESIZE_BOTTOM = 16,
46};
47
48gfx::Point clickedMousePos;
49gfx::Rect* clickedWindowPos = nullptr;
50
51class WindowTitleLabel : public Label {
52public:
53 WindowTitleLabel(const std::string& text) : Label(text) {
54 setDecorative(true);
55 setType(kWindowTitleLabelWidget);
56 initTheme();
57 }
58};
59
60
61// Controls the "X" button in a window to close it.
62class WindowCloseButton : public ButtonBase {
63public:
64 WindowCloseButton()
65 : ButtonBase("", kWindowCloseButtonWidget,
66 kButtonWidget, kButtonWidget) {
67 setDecorative(true);
68 initTheme();
69 }
70
71protected:
72
73 void onClick(Event& ev) override {
74 ButtonBase::onClick(ev);
75 closeWindow();
76 }
77
78 bool onProcessMessage(Message* msg) override {
79 switch (msg->type()) {
80
81 case kSetCursorMessage:
82 ui::set_mouse_cursor(kArrowCursor);
83 return true;
84
85 case kKeyDownMessage:
86 if (window()->shouldProcessEscKeyToCloseWindow() &&
87 static_cast<KeyMessage*>(msg)->scancode() == kKeyEsc) {
88 setSelected(true);
89 return true;
90 }
91 break;
92
93 case kKeyUpMessage:
94 if (window()->shouldProcessEscKeyToCloseWindow() &&
95 static_cast<KeyMessage*>(msg)->scancode() == kKeyEsc) {
96 if (isSelected()) {
97 setSelected(false);
98 closeWindow();
99 return true;
100 }
101 }
102 break;
103 }
104
105 return ButtonBase::onProcessMessage(msg);
106 }
107};
108
109} // anonymous namespace
110
111Window::Window(Type type, const std::string& text)
112 : Widget(kWindowWidget)
113 , m_display(nullptr)
114 , m_closer(nullptr)
115 , m_titleLabel(nullptr)
116 , m_closeButton(nullptr)
117 , m_ownDisplay(false)
118 , m_isDesktop(type == DesktopWindow)
119 , m_isMoveable(!m_isDesktop)
120 , m_isSizeable(!m_isDesktop)
121 , m_isOnTop(false)
122 , m_isWantFocus(true)
123 , m_isForeground(false)
124 , m_isAutoRemap(true)
125{
126 setVisible(false);
127 setAlign(LEFT | MIDDLE);
128 if (type == WithTitleBar) {
129 setText(text);
130 addChild(m_closeButton = new WindowCloseButton);
131 }
132
133 initTheme();
134}
135
136Window::~Window()
137{
138 if (auto man = manager())
139 man->_closeWindow(this, isVisible());
140}
141
142Display* Window::display() const
143{
144 if (m_display)
145 return m_display;
146 else if (auto man = manager())
147 return man->display();
148 else
149 return nullptr;
150}
151
152void Window::setDisplay(Display* display, const bool own)
153{
154 if (m_display)
155 m_display->removeWindow(this);
156
157 m_display = display;
158 m_ownDisplay = own;
159
160 if (m_display)
161 m_display->addWindow(this);
162}
163
164void Window::setAutoRemap(bool state)
165{
166 m_isAutoRemap = state;
167}
168
169void Window::setMoveable(bool state)
170{
171 m_isMoveable = state;
172}
173
174void Window::setSizeable(bool state)
175{
176 m_isSizeable = state;
177}
178
179void Window::setOnTop(bool state)
180{
181 m_isOnTop = state;
182}
183
184void Window::setWantFocus(bool state)
185{
186 m_isWantFocus = state;
187}
188
189HitTest Window::hitTest(const gfx::Point& point)
190{
191 HitTestEvent ev(this, point, HitTestNowhere);
192 onHitTest(ev);
193 return ev.hit();
194}
195
196void Window::loadNativeFrame(const gfx::Rect& frame)
197{
198 m_lastFrame = frame;
199
200 // Just in case the saved value is too small, we can take the value
201 // as invalid.
202 gfx::Size sz = sizeHint() * guiscale();
203 if (display())
204 sz *= display()->scale();
205 if (m_lastFrame.w < sz.w/5 ||
206 m_lastFrame.h < sz.h/5) {
207 m_lastFrame.setSize(sz);
208 }
209}
210
211void Window::onClose(CloseEvent& ev)
212{
213 // Fire Close signal
214 Close(ev);
215}
216
217void Window::onHitTest(HitTestEvent& ev)
218{
219 HitTest ht = HitTestNowhere;
220
221 // If this window is not movable
222 if (!m_isMoveable) {
223 ev.setHit(ht);
224 return;
225 }
226
227 // TODO check why this is necessary, there should be a bug in
228 // the manager where we are receiving mouse events and are not
229 // the top most window.
230 Widget* picked = pick(ev.point());
231 if (picked &&
232 picked != this &&
233 picked->type() != kWindowTitleLabelWidget) {
234 ev.setHit(ht);
235 return;
236 }
237
238 int x = ev.point().x;
239 int y = ev.point().y;
240 gfx::Rect pos = bounds();
241 gfx::Rect cpos = childrenBounds();
242
243 // Move
244 if ((hasText())
245 && (((x >= cpos.x) &&
246 (x < cpos.x2()) &&
247 (y >= pos.y+border().bottom()) &&
248 (y < cpos.y)))) {
249 ht = HitTestCaption;
250 }
251 // Resize
252 else if (m_isSizeable) {
253#ifdef __APPLE__
254 // TODO on macOS we cannot start resize actions on native windows
255 if (ownDisplay()) {
256 ev.setHit(ht);
257 return;
258 }
259#endif
260
261 if ((x >= pos.x) && (x < cpos.x)) {
262 if ((y >= pos.y) && (y < cpos.y))
263 ht = HitTestBorderNW;
264 else if ((y > cpos.y2()-1) && (y <= pos.y2()-1))
265 ht = HitTestBorderSW;
266 else
267 ht = HitTestBorderW;
268 }
269 else if ((y >= pos.y) && (y < cpos.y)) {
270 if ((x >= pos.x) && (x < cpos.x))
271 ht = HitTestBorderNW;
272 else if ((x > cpos.x2()-1) && (x <= pos.x2()-1))
273 ht = HitTestBorderNE;
274 else
275 ht = HitTestBorderN;
276 }
277 else if ((x > cpos.x2()-1) && (x <= pos.x2()-1)) {
278 if ((y >= pos.y) && (y < cpos.y))
279 ht = HitTestBorderNE;
280 else if ((y > cpos.y2()-1) && (y <= pos.y2()-1))
281 ht = HitTestBorderSE;
282 else
283 ht = HitTestBorderE;
284 }
285 else if ((y > cpos.y2()-1) && (y <= pos.y2()-1)) {
286 if ((x >= pos.x) && (x < cpos.x))
287 ht = HitTestBorderSW;
288 else if ((x > cpos.x2()-1) && (x <= pos.x2()-1))
289 ht = HitTestBorderSE;
290 else
291 ht = HitTestBorderS;
292 }
293 }
294 else {
295 // Client area
296 ht = HitTestClient;
297 }
298
299 ev.setHit(ht);
300}
301
302void Window::onOpen(Event& ev)
303{
304 // Fire Open signal
305 Open(ev);
306}
307
308void Window::onWindowResize()
309{
310 // Do nothing
311}
312
313void Window::onWindowMovement()
314{
315 // Do nothing
316}
317
318void Window::remapWindow()
319{
320 if (m_isAutoRemap) {
321 m_isAutoRemap = false;
322 setVisible(true);
323 }
324
325 expandWindow(sizeHint());
326
327 // load layout
328 loadLayout();
329
330 invalidate();
331}
332
333void Window::centerWindow(Display* parentDisplay)
334{
335 if (m_isAutoRemap)
336 remapWindow();
337
338 if (!parentDisplay)
339 parentDisplay = manager()->getDefault()->display();
340
341 ASSERT(parentDisplay);
342
343 if (m_isAutoRemap)
344 remapWindow();
345
346 const gfx::Size displaySize = parentDisplay->size();
347 const gfx::Size windowSize = bounds().size();
348
349 fit_bounds(parentDisplay,
350 this,
351 gfx::Rect(displaySize.w/2 - windowSize.w/2,
352 displaySize.h/2 - windowSize.h/2,
353 windowSize.w, windowSize.h));
354}
355
356void Window::moveWindow(const gfx::Rect& rect)
357{
358 moveWindow(rect, true);
359}
360
361void Window::expandWindow(const gfx::Size& size)
362{
363 const gfx::Rect oldBounds = bounds();
364
365 if (ownDisplay()) {
366 os::Window* nativeWindow = display()->nativeWindow();
367 const int scale = nativeWindow->scale();
368 gfx::Rect frame = nativeWindow->frame();
369 frame.setSize(size * scale);
370 nativeWindow->setFrame(frame);
371 setBounds(gfx::Rect(bounds().origin(), size));
372
373 layout();
374 invalidate();
375 }
376 else {
377 setBounds(gfx::Rect(bounds().origin(), size));
378
379 layout();
380 manager()->invalidateRect(oldBounds);
381 }
382}
383
384void Window::openWindow()
385{
386 if (!parent()) {
387 Manager::getDefault()->_openWindow(this, m_isAutoRemap);
388
389 // Open event
390 Event ev(this);
391 onOpen(ev);
392 }
393}
394
395void Window::openWindowInForeground()
396{
397 m_isForeground = true;
398
399 openWindow();
400
401 Manager::getDefault()->_runModalWindow(this);
402
403 m_isForeground = false;
404}
405
406void Window::closeWindow(Widget* closer)
407{
408 // Close event
409 CloseEvent ev(closer);
410 onBeforeClose(ev);
411 if (ev.canceled())
412 return;
413
414 m_closer = closer;
415 if (m_ownDisplay)
416 m_lastFrame = m_display->nativeWindow()->frame();
417
418 if (auto man = manager())
419 man->_closeWindow(this, true);
420
421 onClose(ev);
422}
423
424bool Window::isTopLevel()
425{
426 Widget* manager = this->manager();
427 if (!manager->children().empty())
428 return (this == UI_FIRST_WIDGET(manager->children()));
429 else
430 return false;
431}
432
433bool Window::onProcessMessage(Message* msg)
434{
435 switch (msg->type()) {
436
437 case kOpenMessage:
438 m_closer = nullptr;
439 break;
440
441 case kCloseMessage:
442 saveLayout();
443 break;
444
445 case kMouseDownMessage: {
446 if (!m_isMoveable)
447 break;
448
449 clickedMousePos = static_cast<MouseMessage*>(msg)->position();
450 m_hitTest = hitTest(clickedMousePos);
451
452 if (m_hitTest != HitTestNowhere &&
453 m_hitTest != HitTestClient) {
454 if (clickedWindowPos == nullptr)
455 clickedWindowPos = new gfx::Rect(bounds());
456 else
457 *clickedWindowPos = bounds();
458
459 // Handle native window action
460 if (ownDisplay()) {
461 os::WindowAction action = os::WindowAction::Cancel;
462 switch (m_hitTest) {
463 case HitTestCaption: action = os::WindowAction::Move; break;
464 case HitTestBorderNW: action = os::WindowAction::ResizeFromTopLeft; break;
465 case HitTestBorderN: action = os::WindowAction::ResizeFromTop; break;
466 case HitTestBorderNE: action = os::WindowAction::ResizeFromTopRight; break;
467 case HitTestBorderW: action = os::WindowAction::ResizeFromLeft; break;
468 case HitTestBorderE: action = os::WindowAction::ResizeFromRight; break;
469 case HitTestBorderSW: action = os::WindowAction::ResizeFromBottomLeft; break;
470 case HitTestBorderS: action = os::WindowAction::ResizeFromBottom; break;
471 case HitTestBorderSE: action = os::WindowAction::ResizeFromBottomRight; break;
472 }
473 if (action != os::WindowAction::Cancel) {
474 display()->nativeWindow()->performWindowAction(action, nullptr);
475
476 // As Window::moveWindow() will not be called, we have to
477 // call onWindowMovement() event from here.
478 if (action == os::WindowAction::Move)
479 onWindowMovement();
480
481 return true;
482 }
483 }
484
485 captureMouse();
486 return true;
487 }
488 else
489 break;
490 }
491
492 case kMouseUpMessage:
493 if (hasCapture()) {
494 releaseMouse();
495 set_mouse_cursor(kArrowCursor);
496
497 if (clickedWindowPos != nullptr) {
498 delete clickedWindowPos;
499 clickedWindowPos = nullptr;
500 }
501
502 m_hitTest = HitTestNowhere;
503 return true;
504 }
505 break;
506
507 case kMouseMoveMessage:
508 if (!m_isMoveable)
509 break;
510
511 // Does it have the mouse captured?
512 if (hasCapture()) {
513 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
514
515 // Reposition/resize
516 if (m_hitTest == HitTestCaption) {
517 int x = clickedWindowPos->x + (mousePos.x - clickedMousePos.x);
518 int y = clickedWindowPos->y + (mousePos.y - clickedMousePos.y);
519 moveWindow(gfx::Rect(x, y,
520 bounds().w,
521 bounds().h), true);
522 }
523 else {
524 int x, y, w, h;
525
526 w = clickedWindowPos->w;
527 h = clickedWindowPos->h;
528
529 bool hitLeft = (m_hitTest == HitTestBorderNW ||
530 m_hitTest == HitTestBorderW ||
531 m_hitTest == HitTestBorderSW);
532 bool hitTop = (m_hitTest == HitTestBorderNW ||
533 m_hitTest == HitTestBorderN ||
534 m_hitTest == HitTestBorderNE);
535 bool hitRight = (m_hitTest == HitTestBorderNE ||
536 m_hitTest == HitTestBorderE ||
537 m_hitTest == HitTestBorderSE);
538 bool hitBottom = (m_hitTest == HitTestBorderSW ||
539 m_hitTest == HitTestBorderS ||
540 m_hitTest == HitTestBorderSE);
541
542 if (hitLeft) {
543 w += clickedMousePos.x - mousePos.x;
544 }
545 else if (hitRight) {
546 w += mousePos.x - clickedMousePos.x;
547 }
548
549 if (hitTop) {
550 h += (clickedMousePos.y - mousePos.y);
551 }
552 else if (hitBottom) {
553 h += (mousePos.y - clickedMousePos.y);
554 }
555
556 limitSize(&w, &h);
557
558 if ((bounds().w != w) ||
559 (bounds().h != h)) {
560 if (hitLeft)
561 x = clickedWindowPos->x - (w - clickedWindowPos->w);
562 else
563 x = bounds().x;
564
565 if (hitTop)
566 y = clickedWindowPos->y - (h - clickedWindowPos->h);
567 else
568 y = bounds().y;
569
570 moveWindow(gfx::Rect(x, y, w, h), false);
571 invalidate();
572 }
573 }
574 }
575 break;
576
577 case kSetCursorMessage:
578 if (m_isMoveable) {
579 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
580 HitTest ht = hitTest(mousePos);
581 CursorType cursor = kArrowCursor;
582
583 switch (ht) {
584 case HitTestCaption: cursor = kArrowCursor; break;
585 case HitTestBorderNW: cursor = kSizeNWCursor; break;
586 case HitTestBorderW: cursor = kSizeWCursor; break;
587 case HitTestBorderSW: cursor = kSizeSWCursor; break;
588 case HitTestBorderNE: cursor = kSizeNECursor; break;
589 case HitTestBorderE: cursor = kSizeECursor; break;
590 case HitTestBorderSE: cursor = kSizeSECursor; break;
591 case HitTestBorderN: cursor = kSizeNCursor; break;
592 case HitTestBorderS: cursor = kSizeSCursor; break;
593 }
594
595 set_mouse_cursor(cursor);
596 return true;
597 }
598 break;
599
600 }
601
602 return Widget::onProcessMessage(msg);
603}
604
605// TODO similar to Manager::onInvalidateRegion
606void Window::onInvalidateRegion(const gfx::Region& region)
607{
608 if (!ownDisplay()) {
609 Widget::onInvalidateRegion(region);
610 return;
611 }
612
613 if (!isVisible() || region.contains(bounds()) == gfx::Region::Out)
614 return;
615
616 Display* display = this->display();
617
618 // Intersect only with window bounds, we don't need to use
619 // getDrawableRegion() because each sub-window in the display will
620 // be processed in the following for() loop
621 gfx::Region reg1;
622 reg1.createIntersection(region, gfx::Region(bounds()));
623
624 // Redraw windows from top to background.
625 for (auto window : display->getWindows()) {
626 // Invalidating the manager only works for the main display, to
627 // invalidate windows you have to invalidate them.
628 if (window->ownDisplay()) {
629 ASSERT(this == window);
630 break;
631 }
632
633 // Invalidate regions of this window
634 window->invalidateRegion(reg1);
635
636 // Clip this window area for the next window.
637 gfx::Region reg2;
638 window->getRegion(reg2);
639 reg1 -= reg2;
640 }
641
642 // TODO we should be able to modify m_updateRegion directly here,
643 // so we avoid the getDrawableRegion() call from
644 // Widget::onInvalidateRegion().
645 if (!reg1.isEmpty())
646 Widget::onInvalidateRegion(reg1);
647}
648
649void Window::onResize(ResizeEvent& ev)
650{
651 windowSetPosition(ev.bounds());
652}
653
654void Window::onSizeHint(SizeHintEvent& ev)
655{
656 Widget* manager = this->manager();
657
658 if (m_isDesktop) {
659 Rect cpos = manager->childrenBounds();
660 ev.setSizeHint(cpos.w, cpos.h);
661 }
662 else {
663 Size maxSize(0, 0);
664 Size reqSize;
665
666 if (m_titleLabel)
667 maxSize.w = maxSize.h = 16*guiscale();
668
669 for (auto child : children()) {
670 if (!child->isDecorative()) {
671 reqSize = child->sizeHint();
672
673 maxSize.w = std::max(maxSize.w, reqSize.w);
674 maxSize.h = std::max(maxSize.h, reqSize.h);
675 }
676 }
677
678 if (m_titleLabel)
679 maxSize.w = std::max(maxSize.w, m_titleLabel->sizeHint().w);
680
681 ev.setSizeHint(maxSize.w + border().width(),
682 maxSize.h + border().height());
683 }
684}
685
686void Window::onBroadcastMouseMessage(const gfx::Point& screenPos,
687 WidgetsList& targets)
688{
689 if (!ownDisplay() || display()->nativeWindow()->frame().contains(screenPos))
690 targets.push_back(this);
691
692 // Continue sending the message to siblings windows until a desktop
693 // or foreground window.
694 if (isForeground() || isDesktop())
695 return;
696
697 Widget* sibling = nextSibling();
698 if (sibling)
699 sibling->broadcastMouseMessage(screenPos, targets);
700}
701
702void Window::onSetText()
703{
704 onBuildTitleLabel();
705 Widget::onSetText();
706 initTheme();
707}
708
709void Window::onBuildTitleLabel()
710{
711 if (text().empty()) {
712 if (m_titleLabel) {
713 removeChild(m_titleLabel);
714 delete m_titleLabel;
715 m_titleLabel = nullptr;
716 }
717 }
718 else {
719 if (!m_titleLabel) {
720 m_titleLabel = new WindowTitleLabel(text());
721 addChild(m_titleLabel);
722 }
723 else {
724 m_titleLabel->setText(text());
725 m_titleLabel->setBounds(
726 gfx::Rect(m_titleLabel->bounds()).setSize(
727 m_titleLabel->sizeHint()));
728 }
729 }
730}
731
732void Window::windowSetPosition(const gfx::Rect& rect)
733{
734 // Copy the new position rectangle
735 setBoundsQuietly(rect);
736 Rect cpos = childrenBounds();
737
738 // Set all the children to the same "cpos"
739 for (auto child : children()) {
740 if (child->isDecorative())
741 child->setDecorativeWidgetBounds();
742 else
743 child->setBounds(cpos);
744 }
745
746 onWindowResize();
747}
748
749void Window::limitSize(int* w, int* h)
750{
751 *w = std::max(*w, border().width());
752 *h = std::max(*h, border().height());
753}
754
755void Window::moveWindow(const gfx::Rect& rect, bool use_blit)
756{
757 Manager* manager = this->manager();
758
759 // Discard enqueued kWinMoveMessage for this window because we are
760 // going to send a new kWinMoveMessage with the latest window
761 // bounds.
762 manager->removeMessagesFor(this, kWinMoveMessage);
763
764 // Get the window's current position
765 Rect old_pos = bounds();
766 int dx = rect.x - old_pos.x;
767 int dy = rect.y - old_pos.y;
768
769 // Get the manager's current position
770 Rect man_pos = manager->bounds();
771
772 // Send a kWinMoveMessage message to the window
773 Message* msg = new Message(kWinMoveMessage);
774 msg->setRecipient(this);
775 manager->enqueueMessage(msg);
776
777 // Get the region & the drawable region of the window
778 Region oldDrawableRegion;
779 getDrawableRegion(oldDrawableRegion, kCutTopWindowsAndUseChildArea);
780
781 // If the size of the window changes...
782 if (old_pos.w != rect.w || old_pos.h != rect.h) {
783 // We have to change the position of all children.
784 windowSetPosition(rect);
785 }
786 else {
787 // We can just displace all the widgets by a delta (new_position -
788 // old_position)...
789 offsetWidgets(dx, dy);
790 }
791
792 // Get the new drawable region of the window (it's new because we
793 // moved the window to "rect")
794 Region newDrawableRegion;
795 getDrawableRegion(newDrawableRegion, kCutTopWindowsAndUseChildArea);
796
797 // First of all, we have to find the manager region to invalidate,
798 // it's the old window drawable region without the new window
799 // drawable region.
800 Region invalidManagerRegion;
801 invalidManagerRegion.createSubtraction(
802 oldDrawableRegion,
803 newDrawableRegion);
804
805 // In second place, we have to setup the window invalid region...
806
807 // If "use_blit" isn't activated, we have to redraw the whole window
808 // (sending kPaintMessage messages) in the new drawable region
809 if (!use_blit) {
810 invalidateRegion(newDrawableRegion);
811 }
812 // If "use_blit" is activated, we can move the old drawable to the
813 // new position (to redraw as little as possible).
814 else {
815 Region reg1;
816 reg1 = newDrawableRegion;
817 reg1.offset(-dx, -dy);
818
819 Region moveableRegion;
820 moveableRegion.createIntersection(oldDrawableRegion, reg1);
821
822 // Move the window's graphics
823 Display* display = this->display();
824 ScreenGraphics g(display);
825 hide_mouse_cursor();
826 {
827 IntersectClip clip(&g, man_pos);
828 if (clip) {
829 ui::move_region(display, moveableRegion, dx, dy);
830 }
831 }
832 show_mouse_cursor();
833
834 reg1.createSubtraction(reg1, moveableRegion);
835 reg1.offset(dx, dy);
836 invalidateRegion(reg1);
837 }
838
839 manager->invalidateRegion(invalidManagerRegion);
840
841 onWindowMovement();
842}
843
844} // namespace ui
845