1// Aseprite
2// Copyright (C) 2018-2021 Igara Studio S.A.
3//
4// This program is distributed under the terms of
5// the End-User License Agreement for Aseprite.
6
7#ifndef APP_UI_DRAGGABLE_WIDGET_H_INCLUDED
8#define APP_UI_DRAGGABLE_WIDGET_H_INCLUDED
9#pragma once
10
11#include "os/surface.h"
12#include "os/system.h"
13#include "ui/graphics.h"
14#include "ui/message.h"
15#include "ui/overlay.h"
16#include "ui/overlay_manager.h"
17#include "ui/paint_event.h"
18#include "ui/system.h"
19#include "ui/view.h"
20
21namespace app {
22
23template<typename Base>
24class DraggableWidget : public Base {
25public:
26 template<typename...Args>
27 DraggableWidget(Args...args) : Base(args...) { }
28
29 bool onProcessMessage(ui::Message* msg) override {
30 switch (msg->type()) {
31
32 case ui::kSetCursorMessage:
33 if (m_floatingOverlay) {
34 const ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg);
35 const gfx::Point mousePos = mouseMsg->position();
36 if (onCanDropItemsOutside() &&
37 !getParentBounds().contains(mousePos)) {
38 ui::set_mouse_cursor(ui::kForbiddenCursor);
39 }
40 else {
41 ui::set_mouse_cursor(ui::kMoveCursor);
42 }
43 return true;
44 }
45 break;
46
47 case ui::kMouseDownMessage: {
48 const bool wasCaptured = this->hasCapture();
49 const bool result = Base::onProcessMessage(msg);
50
51 if (!wasCaptured && this->hasCapture()) {
52 const ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg);
53 const gfx::Point mousePos = mouseMsg->position();
54 m_dragMousePos = mousePos;
55 m_floatingOffset = mouseMsg->position() - this->bounds().origin();
56 m_createFloatingOverlay = true;
57 }
58 return result;
59 }
60
61 case ui::kMouseMoveMessage: {
62 const ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg);
63 const gfx::Point mousePos = mouseMsg->position();
64
65 if (this->hasCapture() && m_createFloatingOverlay) {
66 if (this->manager()->pick(mousePos) != this) {
67 m_createFloatingOverlay = false;
68 if (!m_floatingOverlay)
69 createFloatingOverlay();
70 }
71 }
72
73 if (m_floatingOverlay) {
74 m_floatingOverlay->moveOverlay(mousePos - m_floatingOffset);
75
76 bool inside = true;
77 if (onCanDropItemsOutside()) {
78 inside = getParentBounds().contains(mousePos);
79 if (inside) {
80 if (this->hasFlags(ui::HIDDEN)) {
81 this->disableFlags(ui::HIDDEN);
82 layoutParent();
83 }
84 }
85 else {
86 if (!this->hasFlags(ui::HIDDEN)) {
87 this->enableFlags(ui::HIDDEN);
88 layoutParent();
89 }
90 }
91 }
92
93 onReorderWidgets(mousePos, inside);
94 }
95 break;
96 }
97
98 case ui::kMouseUpMessage: {
99 const ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg);
100 const gfx::Point mousePos = mouseMsg->position();
101
102 m_wasDragged = (this->hasCapture() && m_floatingOverlay);
103 const bool result = Base::onProcessMessage(msg);
104
105 if (!this->hasCapture()) {
106 if (m_floatingOverlay) {
107 destroyFloatingOverlay();
108 ASSERT(!m_createFloatingOverlay);
109 onFinalDrop(getParentBounds().contains(mousePos));
110 }
111 else if (m_createFloatingOverlay)
112 m_createFloatingOverlay = false;
113 }
114
115 m_wasDragged = false;
116 return result;
117 }
118
119 }
120 return Base::onProcessMessage(msg);
121 }
122
123 bool wasDragged() const {
124 return m_wasDragged;
125 }
126
127 bool isDragging() const {
128 return m_isDragging;
129 }
130
131private:
132
133 void createFloatingOverlay() {
134 ASSERT(!m_floatingOverlay);
135
136 m_isDragging = true;
137
138 gfx::Size sz = getFloatingOverlaySize();
139 sz.w = std::max(1, sz.w);
140 sz.h = std::max(1, sz.h);
141 os::SurfaceRef surface = os::instance()->makeRgbaSurface(sz.w, sz.h);
142
143 {
144 os::SurfaceLock lock(surface.get());
145 os::Paint paint;
146 paint.color(gfx::rgba(0, 0, 0, 0));
147 paint.style(os::Paint::Fill);
148 surface->drawRect(gfx::Rect(0, 0, surface->width(), surface->height()), paint);
149 }
150
151 ui::Display* display = this->Base::display();
152 {
153 ui::Graphics g(display, surface, 0, 0);
154 g.setFont(AddRef(this->font()));
155 drawFloatingOverlay(g);
156 }
157
158 m_floatingOverlay = base::make_ref<ui::Overlay>(
159 display, surface, gfx::Point(),
160 (ui::Overlay::ZOrder)(ui::Overlay::MouseZOrder-1));
161 ui::OverlayManager::instance()->addOverlay(m_floatingOverlay);
162 }
163
164 void destroyFloatingOverlay() {
165 ui::OverlayManager::instance()->removeOverlay(m_floatingOverlay);
166 m_floatingOverlay.reset();
167 m_isDragging = false;
168 }
169
170 gfx::Size getFloatingOverlaySize() {
171 auto view = ui::View::getView(this->parent());
172 if (view)
173 return (view->viewportBounds().offset(view->viewScroll()) & this->bounds()).size();
174 else
175 return this->size();
176 }
177
178 gfx::Rect getParentBounds() {
179 auto view = ui::View::getView(this->parent());
180 if (view)
181 return view->viewportBounds();
182 else
183 return this->parent()->bounds();
184 }
185
186 void layoutParent() {
187 this->parent()->layout();
188 auto view = ui::View::getView(this->parent());
189 if (view)
190 return view->updateView();
191 }
192
193 void drawFloatingOverlay(ui::Graphics& g) {
194 ui::PaintEvent ev(this, &g);
195 this->onPaint(ev);
196 }
197
198 virtual bool onCanDropItemsOutside() { return true; }
199 virtual void onReorderWidgets(const gfx::Point& mousePos, bool inside) { }
200 virtual void onFinalDrop(bool inside) { }
201
202 // True if we should create the floating overlay after leaving the
203 // widget bounds.
204 bool m_createFloatingOverlay = false;
205
206 bool m_isDragging = false;
207
208 // True when the mouse button is released (drop operation) and we've
209 // dragged the widget to other position. Can be used to avoid
210 // triggering the default click operation by derived classes when
211 // we've dragged the widget.
212 bool m_wasDragged = false;
213
214 // Initial mouse position when we start the dragging process.
215 gfx::Point m_dragMousePos;
216
217 // Overlay used to show the floating widget (this overlay floats
218 // next to the mouse cursor).
219 ui::OverlayRef m_floatingOverlay;
220
221 // Relative mouse position between the widget and the overlay.
222 gfx::Point m_floatingOffset;
223};
224
225} // namespace app
226
227#endif
228