mirror of
https://github.com/UpsilonNumworks/Upsilon.git
synced 2026-01-19 08:47:28 +01:00
280 lines
10 KiB
C++
280 lines
10 KiB
C++
#include <escher/scroll_view.h>
|
|
#include <escher/palette.h>
|
|
#include <new>
|
|
extern "C" {
|
|
#include <assert.h>
|
|
}
|
|
#include <algorithm>
|
|
|
|
ScrollView::ScrollView(View * contentView, ScrollViewDataSource * dataSource) :
|
|
View(),
|
|
m_contentView(contentView),
|
|
m_dataSource(dataSource),
|
|
m_topMargin(0),
|
|
m_rightMargin(0),
|
|
m_bottomMargin(0),
|
|
m_leftMargin(0),
|
|
m_innerView(this),
|
|
m_decorators(),
|
|
m_backgroundColor(Palette::WallScreen)
|
|
{
|
|
assert(m_dataSource != nullptr);
|
|
setDecoratorType(Decorator::Type::Bars);
|
|
}
|
|
|
|
ScrollView::ScrollView(ScrollView&& other) :
|
|
m_contentView(other.m_contentView),
|
|
m_dataSource(other.m_dataSource),
|
|
m_topMargin(other.m_topMargin),
|
|
m_rightMargin(other.m_rightMargin),
|
|
m_bottomMargin(other.m_bottomMargin),
|
|
m_leftMargin(other.m_leftMargin),
|
|
m_innerView(this),
|
|
m_backgroundColor(other.m_backgroundColor)
|
|
{
|
|
setDecoratorType(other.m_decoratorType);
|
|
}
|
|
|
|
KDSize ScrollView::minimalSizeForOptimalDisplay() const {
|
|
KDSize contentSize = m_contentView->minimalSizeForOptimalDisplay();
|
|
return KDSize(
|
|
contentSize.width() + m_leftMargin + m_rightMargin,
|
|
contentSize.height() + m_topMargin + m_bottomMargin);
|
|
}
|
|
|
|
void ScrollView::setMargins(KDCoordinate top, KDCoordinate right, KDCoordinate bottom, KDCoordinate left) {
|
|
m_topMargin = top;
|
|
m_rightMargin = right;
|
|
m_bottomMargin = bottom;
|
|
m_leftMargin = left;
|
|
}
|
|
|
|
void ScrollView::scrollToContentPoint(KDPoint p, bool allowOverscroll) {
|
|
if (!allowOverscroll && !m_contentView->bounds().contains(p)) {
|
|
return;
|
|
}
|
|
KDCoordinate offsetX = 0;
|
|
KDCoordinate offsetY = 0;
|
|
KDRect visibleRect = visibleContentRect();
|
|
if (visibleRect.left() > p.x()) {
|
|
offsetX = p.x() - visibleRect.left();
|
|
}
|
|
if (visibleRect.right() < p.x()) {
|
|
offsetX = p.x() - visibleRect.right();
|
|
}
|
|
if (visibleRect.top() > p.y()) {
|
|
offsetY = p.y() - visibleRect.top();
|
|
}
|
|
if (visibleRect.bottom() < p.y()) {
|
|
offsetY = p.y() - visibleRect.bottom();
|
|
}
|
|
if (offsetX != 0 || offsetY != 0) {
|
|
setContentOffset(contentOffset().translatedBy(KDPoint(offsetX, offsetY)));
|
|
}
|
|
|
|
// Handle cases when the size of the view has decreased.
|
|
setContentOffset(KDPoint(
|
|
std::min(contentOffset().x(), std::max<KDCoordinate>(minimalSizeForOptimalDisplay().width() - bounds().width(), KDCoordinate{0})),
|
|
std::min(contentOffset().y(), std::max<KDCoordinate>(minimalSizeForOptimalDisplay().height() - bounds().height(), 0))));
|
|
}
|
|
|
|
void ScrollView::scrollToContentRect(KDRect rect, bool allowOverscroll) {
|
|
KDPoint tl = rect.topLeft();
|
|
KDPoint br = rect.bottomRight();
|
|
KDRect visibleRect = visibleContentRect();
|
|
/* We first check that we can display the whole rect. If we can't, we focus
|
|
* the croll to the closest part of the rect. */
|
|
if (visibleRect.height() < rect.height()) {
|
|
// The visible rect is too small to display 'rect'
|
|
if (rect.top() >= visibleRect.top()) {
|
|
// We scroll to display the top part of rect
|
|
br = KDPoint(br.x(), rect.top() + visibleRect.height());
|
|
} else {
|
|
// We scroll to display the bottom part of rect
|
|
tl = KDPoint(tl.x(), rect.bottom() - visibleRect.height());
|
|
}
|
|
}
|
|
if (visibleRect.width() < rect.width()) {
|
|
// The visible rect is too small to display 'rect'
|
|
if (rect.left() >= visibleRect.left()) {
|
|
// We scroll to display the left part of rect
|
|
br = KDPoint(rect.left() + visibleRect.width(), br.y());
|
|
} else {
|
|
// We scroll to display the right part of rect
|
|
tl = KDPoint(rect.right() - visibleRect.width(), tl.y());
|
|
}
|
|
}
|
|
scrollToContentPoint(tl, allowOverscroll);
|
|
scrollToContentPoint(br, allowOverscroll);
|
|
}
|
|
|
|
KDRect ScrollView::visibleContentRect() {
|
|
return KDRect(
|
|
contentOffset().x(),
|
|
contentOffset().y(),
|
|
m_frame.width() - m_leftMargin - m_rightMargin,
|
|
m_frame.height() - m_topMargin - m_bottomMargin);
|
|
}
|
|
|
|
void ScrollView::layoutSubviews(bool force) {
|
|
if (bounds().isEmpty()) {
|
|
return;
|
|
}
|
|
KDRect r1 = KDRectZero;
|
|
KDRect r2 = KDRectZero;
|
|
KDRect innerFrame = decorator()->layoutIndicators(minimalSizeForOptimalDisplay(), contentOffset(), bounds(), &r1, &r2, force);
|
|
if (!r1.isEmpty()) {
|
|
markRectAsDirty(r1);
|
|
}
|
|
if (!r2.isEmpty()) {
|
|
markRectAsDirty(r2);
|
|
}
|
|
m_innerView.setFrame(innerFrame, force);
|
|
KDPoint absoluteOffset = contentOffset().opposite().translatedBy(KDPoint(m_leftMargin - innerFrame.x(), m_topMargin - innerFrame.y()));
|
|
KDRect contentFrame = KDRect(absoluteOffset, contentSize());
|
|
m_contentView->setFrame(contentFrame, force);
|
|
}
|
|
|
|
void ScrollView::setContentOffset(KDPoint offset, bool forceRelayout) {
|
|
if (m_dataSource->setOffset(offset) || forceRelayout) {
|
|
layoutSubviews();
|
|
}
|
|
}
|
|
|
|
void ScrollView::InnerView::drawRect(KDContext * ctx, KDRect rect) const {
|
|
KDCoordinate height = bounds().height();
|
|
KDCoordinate width = bounds().width();
|
|
KDCoordinate offsetX = m_scrollView->contentOffset().x() + m_frame.x();
|
|
KDCoordinate offsetY = m_scrollView->contentOffset().y() + m_frame.y();
|
|
KDCoordinate contentHeight = m_scrollView->m_contentView->bounds().height();
|
|
KDCoordinate contentWidth = m_scrollView->m_contentView->bounds().width();
|
|
ctx->fillRect(KDRect(0, 0, width, m_scrollView->m_topMargin-offsetY), m_scrollView->m_backgroundColor);
|
|
ctx->fillRect(KDRect(0, contentHeight+m_scrollView->m_topMargin-offsetY, width, height - contentHeight - m_scrollView->m_topMargin + offsetY), m_scrollView->m_backgroundColor);
|
|
ctx->fillRect(KDRect(0, 0, m_scrollView->m_leftMargin-offsetX, height), m_scrollView->m_backgroundColor);
|
|
ctx->fillRect(KDRect(contentWidth + m_scrollView->m_leftMargin - offsetX, 0, width - contentWidth - m_scrollView->m_leftMargin + offsetX, height), m_scrollView->m_backgroundColor);
|
|
}
|
|
|
|
View * ScrollView::BarDecorator::indicatorAtIndex(int index) {
|
|
if (index == 1) {
|
|
return &m_verticalBar;
|
|
}
|
|
assert(index == 2);
|
|
return &m_horizontalBar;
|
|
}
|
|
|
|
KDRect ScrollView::BarDecorator::layoutIndicators(KDSize content, KDPoint offset, KDRect frame, KDRect * dirtyRect1, KDRect * dirtyRect2, bool force) {
|
|
bool hBarWasVisible = m_horizontalBar.visible();
|
|
bool hBarIsVisible = m_horizontalBar.update(content.width(), offset.x(), frame.width());
|
|
bool vBarWasVisible = m_verticalBar.visible();
|
|
bool vBarIsVisible = m_verticalBar.update(content.height(), offset.y(), frame.height());
|
|
|
|
KDCoordinate hBarFrameBreadth = k_barsFrameBreadth * hBarIsVisible;
|
|
KDCoordinate vBarFrameBreadth = k_barsFrameBreadth * vBarIsVisible;
|
|
|
|
// Mark the vertical and horizontal rects as dirty id needed
|
|
*dirtyRect1 = hBarWasVisible == hBarIsVisible ? KDRectZero : KDRect(frame.width() - k_barsFrameBreadth, 0, k_barsFrameBreadth, frame.height());
|
|
*dirtyRect2 = vBarWasVisible == vBarIsVisible ? KDRectZero : KDRect(0, frame.height() - k_barsFrameBreadth, frame.width(), k_barsFrameBreadth);
|
|
|
|
/* If the two indicators are visible, we leave an empty rectangle in the right
|
|
* bottom corner. Otherwise, the only indicator uses all the height/width. */
|
|
m_verticalBar.setFrame(KDRect(
|
|
frame.width() - vBarFrameBreadth, 0,
|
|
vBarFrameBreadth, frame.height() - hBarFrameBreadth),
|
|
force);
|
|
m_horizontalBar.setFrame(KDRect(
|
|
0, frame.height() - hBarFrameBreadth,
|
|
frame.width() - vBarFrameBreadth, hBarFrameBreadth),
|
|
force);
|
|
return frame;
|
|
}
|
|
|
|
View * ScrollView::ArrowDecorator::indicatorAtIndex(int index) {
|
|
switch(index) {
|
|
case 1:
|
|
return &m_topArrow;
|
|
case 2:
|
|
return &m_rightArrow;
|
|
case 3:
|
|
return &m_bottomArrow;
|
|
default:
|
|
assert(index == 4);
|
|
return &m_leftArrow;
|
|
}
|
|
}
|
|
|
|
KDRect ScrollView::ArrowDecorator::layoutIndicators(KDSize content, KDPoint offset, KDRect frame, KDRect * dirtyRect1, KDRect * dirtyRect2, bool force) {
|
|
// There is no need to dirty the rects
|
|
KDSize arrowSize = KDFont::LargeFont->glyphSize();
|
|
KDCoordinate topArrowFrameBreadth = arrowSize.height() * m_topArrow.update(0 < offset.y());
|
|
KDCoordinate rightArrowFrameBreadth = arrowSize.width() * m_rightArrow.update(offset.x() + frame.width() < content.width());
|
|
KDCoordinate bottomArrowFrameBreadth = arrowSize.height() * m_bottomArrow.update(offset.y() + frame.height() < content.height());
|
|
KDCoordinate leftArrowFrameBreadth = arrowSize.width() * m_leftArrow.update(0 < offset.x());
|
|
m_topArrow.setFrame(KDRect(
|
|
0, 0,
|
|
frame.width(), topArrowFrameBreadth),
|
|
force);
|
|
m_rightArrow.setFrame(KDRect(
|
|
frame.width() - rightArrowFrameBreadth, 0,
|
|
rightArrowFrameBreadth, frame.height()),
|
|
force);
|
|
m_bottomArrow.setFrame(KDRect(
|
|
0, frame.height() - bottomArrowFrameBreadth,
|
|
frame.width(), bottomArrowFrameBreadth),
|
|
force);
|
|
m_leftArrow.setFrame(KDRect(
|
|
0, 0,
|
|
leftArrowFrameBreadth, frame.height()),
|
|
force);
|
|
return KDRect(
|
|
frame.x() + leftArrowFrameBreadth,
|
|
frame.y() + topArrowFrameBreadth,
|
|
frame.width() - leftArrowFrameBreadth - rightArrowFrameBreadth,
|
|
frame.height() - topArrowFrameBreadth - bottomArrowFrameBreadth
|
|
);
|
|
}
|
|
|
|
void ScrollView::ArrowDecorator::setBackgroundColor(KDColor c) {
|
|
const int indicatorsCount = numberOfIndicators();
|
|
for (int index = 1; index <= indicatorsCount; index++) {
|
|
static_cast<ScrollViewArrow *>(indicatorAtIndex(index))->setBackgroundColor(c);
|
|
}
|
|
}
|
|
|
|
ScrollView::Decorators::Decorators() {
|
|
/* We need to initiate the Union at construction to avoid destructing an
|
|
* uninitialized object when changing the decorator type. */
|
|
new (this) Decorator();
|
|
}
|
|
|
|
ScrollView::Decorators::~Decorators() {
|
|
activeDecorator()->~Decorator();
|
|
}
|
|
|
|
void ScrollView::Decorators::setActiveDecorator(Decorator::Type t) {
|
|
/* Decorator destructor is virtual so calling ~Decorator() on a Decorator
|
|
* pointer will call the appropriate destructor. */
|
|
activeDecorator()->~Decorator();
|
|
switch (t) {
|
|
case Decorator::Type::Bars:
|
|
new (&m_bars) BarDecorator();
|
|
break;
|
|
case Decorator::Type::Arrows:
|
|
new (&m_arrows) ArrowDecorator();
|
|
break;
|
|
default:
|
|
assert(t == Decorator::Type::None);
|
|
new (&m_none) Decorator();
|
|
}
|
|
}
|
|
|
|
#if ESCHER_VIEW_LOGGING
|
|
const char * ScrollView::className() const {
|
|
return "ScrollView";
|
|
}
|
|
|
|
void ScrollView::logAttributes(std::ostream &os) const {
|
|
View::logAttributes(os);
|
|
os << " offset=\"" << (int)contentOffset().x << "," << (int)contentOffset().y << "\"";
|
|
}
|
|
#endif
|