1// Aseprite UI Library
2// Copyright (C) 2019-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/splitter.h"
13
14#include "ui/load_layout_event.h"
15#include "ui/manager.h"
16#include "ui/message.h"
17#include "ui/resize_event.h"
18#include "ui/save_layout_event.h"
19#include "ui/scale.h"
20#include "ui/size_hint_event.h"
21#include "ui/system.h"
22#include "ui/theme.h"
23
24#include <algorithm>
25#include <sstream>
26
27namespace ui {
28
29using namespace gfx;
30
31Splitter::Splitter(Type type, int align)
32 : Widget(kSplitterWidget)
33 , m_type(type)
34 , m_userPos(50)
35 , m_pos(50)
36 , m_guiscale(guiscale())
37{
38 setAlign(align);
39 initTheme();
40}
41
42void Splitter::setPosition(double pos)
43{
44 m_userPos = pos;
45 calcPos();
46 onPositionChange();
47
48 invalidate();
49}
50
51bool Splitter::onProcessMessage(Message* msg)
52{
53 switch (msg->type()) {
54
55 case kMouseDownMessage:
56 if (!isEnabled())
57 break;
58 else {
59 Widget* c1, *c2;
60 int x1, y1, x2, y2;
61 int bar, click_bar;
62 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
63
64 bar = click_bar = 0;
65
66 for (auto it=children().begin(),
67 end=children().end();
68 it != end; ) {
69 auto next = it;
70 ++next;
71
72 if (next != end) {
73 c1 = *it;
74 c2 = *next;
75
76 ++bar;
77
78 if (this->align() & HORIZONTAL) {
79 x1 = c1->bounds().x2();
80 y1 = bounds().y;
81 x2 = c2->bounds().x;
82 y2 = bounds().y2();
83 }
84 else {
85 x1 = bounds().x;
86 y1 = c1->bounds().y2();
87 x2 = bounds().x2();
88 y2 = c2->bounds().y;
89 }
90
91 if ((mousePos.x >= x1) && (mousePos.x < x2) &&
92 (mousePos.y >= y1) && (mousePos.y < y2))
93 click_bar = bar;
94 }
95
96 it = next;
97 }
98
99 if (!click_bar)
100 break;
101
102 captureMouse();
103
104 // Continue with motion message...
105 [[fallthrough]];
106 }
107
108 case kMouseMoveMessage:
109 if (hasCapture()) {
110 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
111
112 if (align() & HORIZONTAL) {
113 switch (m_type) {
114 case ByPercentage:
115 m_userPos = 100.0 * (mousePos.x - bounds().x) / bounds().w;
116 break;
117 case ByPixel:
118 m_userPos = mousePos.x - bounds().x;
119 break;
120 }
121 }
122 else {
123 switch (m_type) {
124 case ByPercentage:
125 m_userPos = 100.0 * (mousePos.y - bounds().y) / bounds().h;
126 break;
127 case ByPixel:
128 m_userPos = mousePos.y - bounds().y;
129 break;
130 }
131 }
132
133 calcPos();
134 onPositionChange();
135 return true;
136 }
137 break;
138
139 case kMouseUpMessage:
140 if (hasCapture()) {
141 releaseMouse();
142 return true;
143 }
144 break;
145
146 case kSetCursorMessage:
147 if (isEnabled() && (!manager()->getCapture() || hasCapture())) {
148 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
149 Widget* c1, *c2;
150 int x1, y1, x2, y2;
151 bool change_cursor = false;
152
153 for (auto it=children().begin(),
154 end=children().end();
155 it != end; ) {
156 auto next = it;
157 ++next;
158
159 if (next != end) {
160 c1 = *it;
161 c2 = *(it+1);
162
163 if (this->align() & HORIZONTAL) {
164 x1 = c1->bounds().x2();
165 y1 = bounds().y;
166 x2 = c2->bounds().x;
167 y2 = bounds().y2();
168 }
169 else {
170 x1 = bounds().x;
171 y1 = c1->bounds().y2();
172 x2 = bounds().x2();
173 y2 = c2->bounds().y;
174 }
175
176 if ((mousePos.x >= x1) && (mousePos.x < x2) &&
177 (mousePos.y >= y1) && (mousePos.y < y2)) {
178 change_cursor = true;
179 break;
180 }
181 }
182
183 it = next;
184 }
185
186 if (change_cursor) {
187 if (align() & HORIZONTAL)
188 set_mouse_cursor(kSizeWECursor);
189 else
190 set_mouse_cursor(kSizeNSCursor);
191 return true;
192 }
193 }
194 break;
195
196 }
197
198 return Widget::onProcessMessage(msg);
199}
200
201void Splitter::onInitTheme(InitThemeEvent& ev)
202{
203 if (m_type == ByPixel) m_pos /= m_guiscale;
204 m_guiscale = ui::guiscale();
205 if (m_type == ByPixel) m_pos *= m_guiscale;
206
207 Widget::onInitTheme(ev);
208}
209
210void Splitter::onResize(ResizeEvent& ev)
211{
212#define LAYOUT_TWO_CHILDREN(x, y, w, h, l, t, r, b) \
213 { \
214 avail = rc.w - childSpacing(); \
215 \
216 pos.x = rc.x; \
217 pos.y = rc.y; \
218 switch (m_type) { \
219 case ByPercentage: \
220 pos.w = int(avail*m_pos/100); \
221 break; \
222 case ByPixel: \
223 pos.w = int(m_pos); \
224 break; \
225 } \
226 \
227 /* TODO uncomment this to make a restricted splitter */ \
228 /* pos.w = std::clamp(pos.w, reqSize1.w, avail-reqSize2.w); */ \
229 pos.h = rc.h; \
230 \
231 child1->setBounds(pos); \
232 gfx::Rect child1Pos = child1->bounds(); \
233 \
234 pos.x = child1Pos.x + child1Pos.w + childSpacing(); \
235 pos.y = rc.y; \
236 pos.w = avail - child1Pos.w; \
237 pos.h = rc.h; \
238 \
239 child2->setBounds(pos); \
240 }
241
242 gfx::Rect rc(ev.bounds());
243 gfx::Rect pos(0, 0, 0, 0);
244 int avail;
245
246 setBoundsQuietly(rc);
247 calcPos();
248
249 Widget* child1 = panel1();
250 Widget* child2 = panel2();
251
252 if (child1 && child2) {
253 if (align() & HORIZONTAL) {
254 LAYOUT_TWO_CHILDREN(x, y, w, h, l, t, r, b);
255 }
256 else {
257 LAYOUT_TWO_CHILDREN(y, x, h, w, t, l, b, r);
258 }
259 }
260 else if (child1)
261 child1->setBounds(rc);
262 else if (child2)
263 child2->setBounds(rc);
264}
265
266void Splitter::onSizeHint(SizeHintEvent& ev)
267{
268#define GET_CHILD_SIZE(w, h) \
269 do { \
270 w = std::max(w, reqSize.w); \
271 h = std::max(h, reqSize.h); \
272 } while(0)
273
274#define FINAL_SIZE(w) \
275 do { \
276 w *= visibleChildren; \
277 w += childSpacing() * (visibleChildren-1); \
278 } while(0)
279
280 int visibleChildren;
281 Size reqSize;
282
283 visibleChildren = 0;
284 for (auto child : children()) {
285 if (child->isVisible())
286 visibleChildren++;
287 }
288
289 int w, h;
290 w = h = 0;
291
292 for (auto child : children()) {
293 if (!child->isVisible())
294 continue;
295
296 reqSize = child->sizeHint();
297
298 if (this->align() & HORIZONTAL)
299 GET_CHILD_SIZE(w, h);
300 else
301 GET_CHILD_SIZE(h, w);
302 }
303
304 if (visibleChildren > 0) {
305 if (this->align() & HORIZONTAL)
306 FINAL_SIZE(w);
307 else
308 FINAL_SIZE(h);
309 }
310
311 w += border().width();
312 h += border().height();
313
314 ev.setSizeHint(Size(w, h));
315}
316
317void Splitter::onLoadLayout(LoadLayoutEvent& ev)
318{
319 ev.stream() >> m_userPos;
320 if (m_userPos < 0) m_userPos = 0;
321 if (m_type == ByPixel)
322 m_userPos *= m_guiscale;
323
324 calcPos();
325}
326
327void Splitter::onSaveLayout(SaveLayoutEvent& ev)
328{
329 double pos = (m_type == ByPixel ? m_userPos / m_guiscale:
330 m_userPos);
331 ev.stream() << pos;
332}
333
334void Splitter::onPositionChange()
335{
336 layout();
337}
338
339Widget* Splitter::panel1() const
340{
341 const WidgetsList& list = children();
342 if (list.size() >= 1 && list[0]->isVisible())
343 return list[0];
344 else
345 return nullptr;
346}
347
348Widget* Splitter::panel2() const
349{
350 const WidgetsList& list = children();
351 if (list.size() >= 2 && list[1]->isVisible())
352 return list[1];
353 else
354 return nullptr;
355}
356
357void Splitter::calcPos()
358{
359 if (align() & HORIZONTAL) {
360 switch (m_type) {
361 case ByPercentage:
362 m_pos = std::clamp<double>(m_userPos, 0, 100);
363 break;
364 case ByPixel:
365 if (isVisible())
366 m_pos = std::clamp<double>(m_userPos, 0, bounds().w);
367 break;
368 }
369 }
370 else {
371 switch (m_type) {
372 case ByPercentage:
373 m_pos = std::clamp<double>(m_userPos, 0, 100);
374 break;
375 case ByPixel:
376 if (isVisible())
377 m_pos = std::clamp<double>(m_userPos, 0, bounds().h);
378 break;
379 }
380 }
381}
382
383} // namespace ui
384