aseprite/src/ui/view.cpp
David Capello c42c5e1453 Backport new laf API to main branch of aseprite
Some features from the beta branch of aseprite & laf were backported
to the main branch of aseprite.

Related commits:
- New memory handling (db4504e816ffccf0ea63a78737ebb6e22cc0453b)
- New get event with timeout (e6ec13cc31e6e689040bc651f98ee1752834d14c)
- Convert os::NativeCursor to an enum (06a5b4f3aebfafb6363ea33d349975d6e419ca7b)
- Adapt code to the new os::Display -> os::Window refactor (5d31314cdb23f314391e5eaebd7cea84f5179ac7)
- Save/load main window layout correctly and limit to current workarea (d6acb9e20f11fda938959c99285fe4f7d7051794)
- Redraw window immediately on "live resizing" (d0b39ebade7736d47e6b2450bf68b088c0da8e57)
2021-07-05 17:51:29 -03:00

398 lines
9.6 KiB
C++

// Aseprite UI Library
// Copyright (C) 2018-2021 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/surface.h"
#include "os/window.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