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// #define DEBUG_SCROLL_EVENTS
9
10#ifdef HAVE_CONFIG_H
11#include "config.h"
12#endif
13
14#include "gfx/size.h"
15#include "ui/display.h"
16#include "ui/intern.h"
17#include "ui/message.h"
18#include "ui/move_region.h"
19#include "ui/resize_event.h"
20#include "ui/scroll_helper.h"
21#include "ui/scroll_region_event.h"
22#include "ui/size_hint_event.h"
23#include "ui/system.h"
24#include "ui/theme.h"
25#include "ui/view.h"
26#include "ui/widget.h"
27
28#ifdef DEBUG_SCROLL_EVENTS
29#include "base/thread.h"
30#include "os/surface.h"
31#include "os/window.h"
32#endif
33
34#include <algorithm>
35#include <queue>
36
37#define HBAR_SIZE (m_scrollbar_h.getBarWidth())
38#define VBAR_SIZE (m_scrollbar_v.getBarWidth())
39
40namespace ui {
41
42using namespace gfx;
43
44View::View()
45 : Widget(kViewWidget)
46 , m_scrollbar_h(HORIZONTAL, this)
47 , m_scrollbar_v(VERTICAL, this)
48{
49 m_hasBars = true;
50
51 enableFlags(IGNORE_MOUSE);
52 setFocusStop(true);
53 addChild(&m_viewport);
54 setScrollableSize(Size(0, 0));
55
56 initTheme();
57}
58
59bool View::hasScrollBars()
60{
61 return m_hasBars;
62}
63
64void View::attachToView(Widget* viewable_widget)
65{
66 m_viewport.addChild(viewable_widget);
67}
68
69Widget* View::attachedWidget()
70{
71 return UI_FIRST_WIDGET(m_viewport.children());
72}
73
74void View::makeVisibleAllScrollableArea()
75{
76 Size reqSize = m_viewport.calculateNeededSize();
77
78 setMinSize(
79 gfx::Size(
80 + reqSize.w
81 + m_viewport.border().width()
82 + border().width(),
83
84 + reqSize.h
85 + m_viewport.border().height()
86 + border().height()));
87}
88
89void View::hideScrollBars()
90{
91 m_hasBars = false;
92 updateView();
93}
94
95void View::showScrollBars()
96{
97 m_hasBars = true;
98 updateView();
99}
100
101Size View::getScrollableSize() const
102{
103 return Size(m_scrollbar_h.size(),
104 m_scrollbar_v.size());
105}
106
107void View::setScrollableSize(const gfx::Size& sz,
108 const bool setScrollPos)
109{
110 gfx::Rect viewportArea = childrenBounds();
111
112 if (m_hasBars) {
113 setup_scrollbars(sz,
114 viewportArea,
115 *this,
116 m_scrollbar_h,
117 m_scrollbar_v);
118 }
119 else {
120 if (m_scrollbar_h.parent()) removeChild(&m_scrollbar_h);
121 if (m_scrollbar_v.parent()) removeChild(&m_scrollbar_v);
122 m_scrollbar_h.setVisible(false);
123 m_scrollbar_v.setVisible(false);
124 m_scrollbar_h.setSize(sz.w);
125 m_scrollbar_v.setSize(sz.h);
126 }
127 m_viewport.setBoundsQuietly(viewportArea);
128
129 // Setup viewport
130 if (setScrollPos) {
131 setViewScroll(viewScroll()); // Setup the same scroll-point
132 invalidate();
133 }
134}
135
136Size View::visibleSize() const
137{
138 return Size(m_viewport.bounds().w - m_viewport.border().width(),
139 m_viewport.bounds().h - m_viewport.border().height());
140}
141
142Point View::viewScroll() const
143{
144 return Point(m_scrollbar_h.getPos(),
145 m_scrollbar_v.getPos());
146}
147
148void View::setViewScroll(const Point& pt)
149{
150 onSetViewScroll(pt);
151}
152
153// If restoreScrollPos=false it means that the caller of
154// updateView(false) will then update the view scroll position
155// manually.
156void View::updateView(const bool restoreScrollPos)
157{
158 Widget* vw = UI_FIRST_WIDGET(m_viewport.children());
159 Point scroll = viewScroll();
160
161 // Set minimum (remove scroll-bars)
162 setScrollableSize(Size(0, 0), false);
163
164 // Set needed size
165 setScrollableSize(m_viewport.calculateNeededSize(), false);
166
167 // If there are scroll-bars, we have to setup the scrollable-size
168 // again (because they remove visible space, maybe now we need a
169 // vertical or horizontal bar too).
170 if (hasChild(&m_scrollbar_h) || hasChild(&m_scrollbar_v))
171 setScrollableSize(m_viewport.calculateNeededSize(), false);
172
173 m_viewport.setBounds(m_viewport.bounds());
174 if (restoreScrollPos) {
175 if (vw)
176 setViewScroll(scroll);
177 else
178 setViewScroll(Point(0, 0));
179 }
180
181 if (Widget* child = attachedWidget()) {
182 (void)child;
183 updateAttachedWidgetBounds(viewScroll());
184 ASSERT(child->bounds().w >= viewportBounds().w);
185 ASSERT(child->bounds().h >= viewportBounds().h);
186 }
187
188 invalidate();
189}
190
191Viewport* View::viewport()
192{
193 return &m_viewport;
194}
195
196Rect View::viewportBounds()
197{
198 return m_viewport.bounds() - m_viewport.border();
199}
200
201// static
202View* View::getView(const Widget* widget)
203{
204 if ((widget->parent()) &&
205 (widget->parent()->type() == kViewViewportWidget) &&
206 (widget->parent()->parent()) &&
207 (widget->parent()->parent()->type() == kViewWidget))
208 return static_cast<View*>(widget->parent()->parent());
209 else
210 return 0;
211}
212
213bool View::onProcessMessage(Message* msg)
214{
215 switch (msg->type()) {
216
217 case kFocusEnterMessage:
218 case kFocusLeaveMessage:
219 // TODO This is theme specific stuff
220 // Redraw the borders each time the focus enters or leaves the view.
221 {
222 Region region;
223 getDrawableRegion(region, kCutTopWindows);
224 invalidateRegion(region);
225 }
226 break;
227 }
228
229 return Widget::onProcessMessage(msg);
230}
231
232void View::onInitTheme(InitThemeEvent& ev)
233{
234 m_viewport.initTheme();
235 m_scrollbar_h.initTheme();
236 m_scrollbar_v.initTheme();
237
238 Widget::onInitTheme(ev);
239}
240
241void View::onResize(ResizeEvent& ev)
242{
243 setBoundsQuietly(ev.bounds());
244 updateView();
245}
246
247void View::onSizeHint(SizeHintEvent& ev)
248{
249 Widget::onSizeHint(ev);
250 gfx::Size sz = ev.sizeHint();
251 sz += m_viewport.sizeHint();
252 ev.setSizeHint(sz);
253}
254
255void View::onSetViewScroll(const gfx::Point& pt)
256{
257 // If the view is not visible, we don't adjust any screen region.
258 if (!isVisible())
259 return;
260
261 Point oldScroll = viewScroll();
262 Point newScroll = limitScrollPosToViewport(pt);
263 if (newScroll == oldScroll)
264 return;
265
266 // Visible viewport region that is not overlapped by windows
267 Region drawableRegion;
268 m_viewport.getDrawableRegion(drawableRegion, kCutTopWindowsAndUseChildArea);
269
270 // Start the region to scroll equal to the drawable viewport region.
271 Rect cpos = m_viewport.childrenBounds();
272 Region validRegion(cpos);
273 validRegion &= drawableRegion;
274
275 // Remove all children invalid regions from this "validRegion"
276 {
277 std::queue<Widget*> items;
278 items.push(&m_viewport);
279 while (!items.empty()) {
280 Widget* item = items.front();
281 items.pop();
282 for (Widget* child : item->children())
283 items.push(child);
284
285 if (item->isVisible())
286 validRegion -= item->getUpdateRegion();
287 }
288 }
289
290 // Remove invalid region in the screen (areas that weren't
291 // re-painted yet)
292 Display* display = this->display();
293 if (display)
294 validRegion -= display->getInvalidRegion();
295
296 // Add extra regions that cannot be scrolled (this can be customized
297 // by subclassing ui::View). We use two ScrollRegionEvent, this
298 // first one with the old scroll position. And the next one with the
299 // new scroll position.
300 {
301 ScrollRegionEvent ev(this, validRegion);
302 onScrollRegion(ev);
303 }
304
305 // Move attached widget
306 updateAttachedWidgetBounds(newScroll);
307
308 // Change scroll bar positions
309 m_scrollbar_h.setPos(newScroll.x);
310 m_scrollbar_v.setPos(newScroll.y);
311
312 // Region to invalidate (new visible children/child parts)
313 Region invalidRegion(cpos);
314 invalidRegion &= drawableRegion;
315
316 // Move the valid screen region. "delta" is the movement for the
317 // scrolled region (which is inverse to the scroll position
318 // delta/movement).
319 const Point delta = oldScroll - newScroll;
320 {
321 // The movable region includes the given "validRegion"
322 // intersecting itself when it's in the new position, so we don't
323 // overlap regions outside the "validRegion".
324 Region movable = validRegion;
325 movable.offset(delta);
326 movable &= validRegion;
327 invalidRegion -= movable; // Remove the moved region as invalid
328 movable.offset(-delta);
329
330 ui::move_region(display, movable, delta.x, delta.y);
331 }
332
333#ifdef DEBUG_SCROLL_EVENTS
334 // Paint invalid region with red fill
335 if (auto nativeWindow = display->nativeWindow()) {
336 nativeWindow->invalidateRegion(gfx::Region(display->bounds()));
337 base::this_thread::sleep_for(0.002);
338 {
339 os::Surface* surface = nativeWindow->surface();
340 os::SurfaceLock lock(surface);
341 os::Paint p;
342 p.style(os::Paint::Fill);
343 p.color(gfx::rgba(255, 0, 0));
344 for (const auto& rc : invalidRegion)
345 surface->drawRect(rc, p);
346 }
347 nativeWindow->invalidateRegion(gfx::Region(display->bounds()));
348 base::this_thread::sleep_for(0.02);
349 }
350#endif
351
352 // Invalidate viewport's children regions
353 m_viewport.invalidateRegion(invalidRegion);
354
355 // Notify about the new scroll position
356 onScrollChange();
357}
358
359void View::onScrollRegion(ScrollRegionEvent& ev)
360{
361 if (auto viewable = dynamic_cast<ViewableWidget*>(attachedWidget()))
362 viewable->onScrollRegion(ev);
363}
364
365void View::onScrollChange()
366{
367 // Do nothing
368}
369
370void View::updateAttachedWidgetBounds(const gfx::Point& scrollPos)
371{
372 Rect cpos = m_viewport.childrenBounds();
373 cpos.offset(-scrollPos);
374 for (auto child : m_viewport.children()) {
375 Size reqSize = child->sizeHint();
376 cpos.w = std::max(reqSize.w, cpos.w);
377 cpos.h = std::max(reqSize.h, cpos.h);
378 if (cpos.w != child->bounds().w ||
379 cpos.h != child->bounds().h)
380 child->setBounds(cpos);
381 else
382 child->offsetWidgets(cpos.x - child->bounds().x,
383 cpos.y - child->bounds().y);
384 }
385}
386
387gfx::Point View::limitScrollPosToViewport(const gfx::Point& pt) const
388{
389 const Size maxSize = getScrollableSize();
390 const Size visible = visibleSize();
391 return Point(std::clamp(pt.x, 0, std::max(0, maxSize.w - visible.w)),
392 std::clamp(pt.y, 0, std::max(0, maxSize.h - visible.h)));
393}
394
395} // namespace ui
396