aseprite/src/ui/view.cpp
David Capello d5f0e3189c Fix bug drawing and zooming with the mouse wheel or pinch gesture
The mouse position jumps from one side to other when we zoom because
there were an intermediate scroll change event where the mouse
position is converted using the old zoom.

Fixed regressiong from 951fb7c35784d3e5b0aba86e340aed9edbd2456d
Fixed bug https://community.aseprite.org/t/4587
2020-01-13 15:01:45 -03:00

398 lines
9.6 KiB
C++

// Aseprite UI Library
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
// #define DEBUG_SCROLL_EVENTS
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "base/clamp.h"
#include "gfx/size.h"
#include "ui/intern.h"
#include "ui/manager.h"
#include "ui/message.h"
#include "ui/move_region.h"
#include "ui/resize_event.h"
#include "ui/scroll_helper.h"
#include "ui/scroll_region_event.h"
#include "ui/size_hint_event.h"
#include "ui/system.h"
#include "ui/theme.h"
#include "ui/view.h"
#include "ui/widget.h"
#ifdef DEBUG_SCROLL_EVENTS
#include "base/thread.h"
#include "os/display.h"
#include "os/surface.h"
#endif
#include <algorithm>
#include <queue>
#define HBAR_SIZE (m_scrollbar_h.getBarWidth())
#define VBAR_SIZE (m_scrollbar_v.getBarWidth())
namespace ui {
using namespace gfx;
View::View()
: Widget(kViewWidget)
, m_scrollbar_h(HORIZONTAL, this)
, m_scrollbar_v(VERTICAL, this)
{
m_hasBars = true;
enableFlags(IGNORE_MOUSE);
setFocusStop(true);
addChild(&m_viewport);
setScrollableSize(Size(0, 0));
initTheme();
}
bool View::hasScrollBars()
{
return m_hasBars;
}
void View::attachToView(Widget* viewable_widget)
{
m_viewport.addChild(viewable_widget);
}
Widget* View::attachedWidget()
{
return UI_FIRST_WIDGET(m_viewport.children());
}
void View::makeVisibleAllScrollableArea()
{
Size reqSize = m_viewport.calculateNeededSize();
setMinSize(
gfx::Size(
+ reqSize.w
+ m_viewport.border().width()
+ border().width(),
+ reqSize.h
+ m_viewport.border().height()
+ border().height()));
}
void View::hideScrollBars()
{
m_hasBars = false;
updateView();
}
void View::showScrollBars()
{
m_hasBars = true;
updateView();
}
Size View::getScrollableSize() const
{
return Size(m_scrollbar_h.size(),
m_scrollbar_v.size());
}
void View::setScrollableSize(const gfx::Size& sz,
const bool setScrollPos)
{
gfx::Rect viewportArea = childrenBounds();
if (m_hasBars) {
setup_scrollbars(sz,
viewportArea,
*this,
m_scrollbar_h,
m_scrollbar_v);
}
else {
if (m_scrollbar_h.parent()) removeChild(&m_scrollbar_h);
if (m_scrollbar_v.parent()) removeChild(&m_scrollbar_v);
m_scrollbar_h.setVisible(false);
m_scrollbar_v.setVisible(false);
m_scrollbar_h.setSize(sz.w);
m_scrollbar_v.setSize(sz.h);
}
m_viewport.setBoundsQuietly(viewportArea);
// Setup viewport
if (setScrollPos) {
setViewScroll(viewScroll()); // Setup the same scroll-point
invalidate();
}
}
Size View::visibleSize() const
{
return Size(m_viewport.bounds().w - m_viewport.border().width(),
m_viewport.bounds().h - m_viewport.border().height());
}
Point View::viewScroll() const
{
return Point(m_scrollbar_h.getPos(),
m_scrollbar_v.getPos());
}
void View::setViewScroll(const Point& pt)
{
onSetViewScroll(pt);
}
// If restoreScrollPos=false it means that the caller of
// updateView(false) will then update the view scroll position
// manually.
void View::updateView(const bool restoreScrollPos)
{
Widget* vw = UI_FIRST_WIDGET(m_viewport.children());
Point scroll = viewScroll();
// Set minimum (remove scroll-bars)
setScrollableSize(Size(0, 0), false);
// Set needed size
setScrollableSize(m_viewport.calculateNeededSize(), false);
// If there are scroll-bars, we have to setup the scrollable-size
// again (because they remove visible space, maybe now we need a
// vertical or horizontal bar too).
if (hasChild(&m_scrollbar_h) || hasChild(&m_scrollbar_v))
setScrollableSize(m_viewport.calculateNeededSize(), false);
m_viewport.setBounds(m_viewport.bounds());
if (restoreScrollPos) {
if (vw)
setViewScroll(scroll);
else
setViewScroll(Point(0, 0));
}
if (Widget* child = attachedWidget()) {
updateAttachedWidgetBounds(viewScroll());
ASSERT(child->bounds().w >= viewportBounds().w);
ASSERT(child->bounds().h >= viewportBounds().h);
}
invalidate();
}
Viewport* View::viewport()
{
return &m_viewport;
}
Rect View::viewportBounds()
{
return m_viewport.bounds() - m_viewport.border();
}
// static
View* View::getView(const Widget* widget)
{
if ((widget->parent()) &&
(widget->parent()->type() == kViewViewportWidget) &&
(widget->parent()->parent()) &&
(widget->parent()->parent()->type() == kViewWidget))
return static_cast<View*>(widget->parent()->parent());
else
return 0;
}
bool View::onProcessMessage(Message* msg)
{
switch (msg->type()) {
case kFocusEnterMessage:
case kFocusLeaveMessage:
// TODO This is theme specific stuff
// Redraw the borders each time the focus enters or leaves the view.
{
Region region;
getDrawableRegion(region, kCutTopWindows);
invalidateRegion(region);
}
break;
}
return Widget::onProcessMessage(msg);
}
void View::onInitTheme(InitThemeEvent& ev)
{
m_viewport.initTheme();
m_scrollbar_h.initTheme();
m_scrollbar_v.initTheme();
Widget::onInitTheme(ev);
}
void View::onResize(ResizeEvent& ev)
{
setBoundsQuietly(ev.bounds());
updateView();
}
void View::onSizeHint(SizeHintEvent& ev)
{
Widget::onSizeHint(ev);
gfx::Size sz = ev.sizeHint();
sz += m_viewport.sizeHint();
ev.setSizeHint(sz);
}
void View::onSetViewScroll(const gfx::Point& pt)
{
// If the view is not visible, we don't adjust any screen region.
if (!isVisible())
return;
Point oldScroll = viewScroll();
Point newScroll = limitScrollPosToViewport(pt);
if (newScroll == oldScroll)
return;
// Visible viewport region that is not overlapped by windows
Region drawableRegion;
m_viewport.getDrawableRegion(drawableRegion, kCutTopWindowsAndUseChildArea);
// Start the region to scroll equal to the drawable viewport region.
Rect cpos = m_viewport.childrenBounds();
Region validRegion(cpos);
validRegion &= drawableRegion;
// Remove all children invalid regions from this "validRegion"
{
std::queue<Widget*> items;
items.push(&m_viewport);
while (!items.empty()) {
Widget* item = items.front();
items.pop();
for (Widget* child : item->children())
items.push(child);
if (item->isVisible())
validRegion -= item->getUpdateRegion();
}
}
// Remove invalid region in the screen (areas that weren't
// re-painted yet)
Manager* manager = this->manager();
if (manager)
validRegion -= manager->getInvalidRegion();
// Add extra regions that cannot be scrolled (this can be customized
// by subclassing ui::View). We use two ScrollRegionEvent, this
// first one with the old scroll position. And the next one with the
// new scroll position.
{
ScrollRegionEvent ev(this, validRegion);
onScrollRegion(ev);
}
// Move attached widget
updateAttachedWidgetBounds(newScroll);
// Change scroll bar positions
m_scrollbar_h.setPos(newScroll.x);
m_scrollbar_v.setPos(newScroll.y);
// Region to invalidate (new visible children/child parts)
Region invalidRegion(cpos);
invalidRegion &= drawableRegion;
// Move the valid screen region. "delta" is the movement for the
// scrolled region (which is inverse to the scroll position
// delta/movement).
const Point delta = oldScroll - newScroll;
{
// The movable region includes the given "validRegion"
// intersecting itself when it's in the new position, so we don't
// overlap regions outside the "validRegion".
Region movable = validRegion;
movable.offset(delta);
movable &= validRegion;
invalidRegion -= movable; // Remove the moved region as invalid
movable.offset(-delta);
ui::move_region(manager, movable, delta.x, delta.y);
}
#ifdef DEBUG_SCROLL_EVENTS
// Paint invalid region with red fill
{
auto display = manager->getDisplay();
if (display)
display->invalidateRegion(
gfx::Region(gfx::Rect(0, 0, display_w(), display_h())));
base::this_thread::sleep_for(0.002);
{
os::Surface* surface = display->getSurface();
os::SurfaceLock lock(surface);
for (const auto& rc : invalidRegion)
surface->fillRect(gfx::rgba(255, 0, 0), rc);
}
if (display)
display->invalidateRegion(
gfx::Region(gfx::Rect(0, 0, display_w(), display_h())));
base::this_thread::sleep_for(0.002);
}
#endif
// Invalidate viewport's children regions
m_viewport.invalidateRegion(invalidRegion);
// Notify about the new scroll position
onScrollChange();
}
void View::onScrollRegion(ScrollRegionEvent& ev)
{
if (auto viewable = dynamic_cast<ViewableWidget*>(attachedWidget()))
viewable->onScrollRegion(ev);
}
void View::onScrollChange()
{
// Do nothing
}
void View::updateAttachedWidgetBounds(const gfx::Point& scrollPos)
{
Rect cpos = m_viewport.childrenBounds();
cpos.offset(-scrollPos);
for (auto child : m_viewport.children()) {
Size reqSize = child->sizeHint();
cpos.w = std::max(reqSize.w, cpos.w);
cpos.h = std::max(reqSize.h, cpos.h);
if (cpos.w != child->bounds().w ||
cpos.h != child->bounds().h)
child->setBounds(cpos);
else
child->offsetWidgets(cpos.x - child->bounds().x,
cpos.y - child->bounds().y);
}
}
gfx::Point View::limitScrollPosToViewport(const gfx::Point& pt) const
{
const Size maxSize = getScrollableSize();
const Size visible = visibleSize();
return Point(base::clamp(pt.x, 0, std::max(0, maxSize.w - visible.w)),
base::clamp(pt.y, 0, std::max(0, maxSize.h - visible.h)));
}
} // namespace ui