mirror of
https://github.com/aseprite/aseprite.git
synced 2025-04-01 01:20:25 +00:00
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)
398 lines
9.6 KiB
C++
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
|