osb/source/windowing/StarScrollArea.cpp
2023-06-20 14:33:09 +10:00

446 lines
14 KiB
C++

#include "StarScrollArea.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
namespace Star {
// TODO: I hate these hardcoded values. Please smite with fire.
static int const ScrollAreaBorder = 9;
static int const ScrollButtonStackSize = 6;
static int const ScrollThumbSize = 3;
static int const ScrollThumbOverhead = ScrollThumbSize + ScrollThumbSize;
static int const ScrollBarTrackOverhead = ScrollButtonStackSize + ScrollButtonStackSize + ScrollThumbOverhead;
static int64_t const ScrollAdvanceTimer = 100;
ScrollThumb::ScrollThumb(GuiDirection direction) {
m_hovered = false;
m_pressed = false;
m_direction = direction;
auto assets = Root::singleton().assets();
setImages(assets->json("/interface.config:scrollArea.thumbs"));
}
void ScrollThumb::setImages(ImageStretchSet const& base, ImageStretchSet const& hover, ImageStretchSet const& pressed) {
m_baseThumb = base;
m_hoverThumb = hover;
m_pressedThumb = pressed;
}
void ScrollThumb::setImages(Json const& images) {
String directionString;
if (m_direction == GuiDirection::Vertical) {
directionString = "vertical";
} else {
directionString = "horizontal";
}
m_baseThumb.begin = images.get(directionString).get("base").getString("begin");
m_baseThumb.end = images.get(directionString).get("base").getString("end");
m_baseThumb.inner = images.get(directionString).get("base").getString("inner");
m_hoverThumb.begin = images.get(directionString).get("hover").getString("begin");
m_hoverThumb.end = images.get(directionString).get("hover").getString("end");
m_hoverThumb.inner = images.get(directionString).get("hover").getString("inner");
m_pressedThumb.begin = images.get(directionString).get("pressed").getString("begin");
m_pressedThumb.end = images.get(directionString).get("pressed").getString("end");
m_pressedThumb.inner = images.get(directionString).get("pressed").getString("inner");
}
bool ScrollThumb::isHovered() const {
return m_hovered;
}
bool ScrollThumb::isPressed() const {
return m_pressed;
}
void ScrollThumb::setHovered(bool hovered) {
m_hovered = hovered;
}
void ScrollThumb::setPressed(bool pressed) {
m_pressed = pressed;
}
void ScrollThumb::mouseOver() {
setHovered(true);
}
void ScrollThumb::mouseOut() {
setHovered(false);
}
void ScrollThumb::renderImpl() {
ImageStretchSet workingSet = m_baseThumb;
if (isHovered())
workingSet = m_hoverThumb;
if (isPressed())
workingSet = m_pressedThumb;
if (workingSet.fullyPopulated()) {
context()->drawImageStretchSet(workingSet,
RectF::withSize(Vec2F(m_parent->screenPosition() + position()), Vec2F(size())),
m_direction);
}
}
Vec2U ScrollThumb::baseSize() const {
return context()->textureSize(m_baseThumb.begin);
}
ScrollBar::ScrollBar(GuiDirection direction, WidgetCallbackFunc forwardFunc, WidgetCallbackFunc backwardFunc)
: m_direction(direction) {
m_forward = make_shared<ButtonWidget>();
m_forward->setCallback(forwardFunc);
m_forward->setSustainCallbackOnDownHold(true);
m_forward->setPressedOffset({0, 0});
m_backward = make_shared<ButtonWidget>();
m_backward->setCallback(backwardFunc);
m_backward->setSustainCallbackOnDownHold(true);
m_backward->setPressedOffset({0, 0});
m_thumb = make_shared<ScrollThumb>(m_direction);
auto assets = Root::singleton().assets();
setButtonImages(assets->json("/interface.config:scrollArea.buttons"));
addChild("thumb", m_thumb);
addChild("forward", m_forward);
addChild("backward", m_backward);
}
void ScrollBar::setButtonImages(Json const& images) {
String directionString;
if (m_direction == GuiDirection::Vertical) {
directionString = "vertical";
} else {
directionString = "horizontal";
}
m_forward->setImages(images.get(directionString).get("forward").getString("base"),
images.get(directionString).get("forward").getString("hover"),
images.get(directionString).get("forward").getString("pressed"));
m_backward->setImages(images.get(directionString).get("backward").getString("base"),
images.get(directionString).get("backward").getString("hover"),
images.get(directionString).get("backward").getString("pressed"));
}
Vec2I ScrollBar::size() const {
if (m_parent)
return m_parent->size();
return {};
}
int ScrollBar::trackSize() const {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
auto size = scrollArea->size();
if (m_direction == GuiDirection::Vertical) {
if (scrollArea->horizontalScroll()) {
return size[1] - (ScrollBarTrackOverhead + ScrollAreaBorder);
} else {
return size[1] - ScrollBarTrackOverhead;
}
} else {
return size[0] - ScrollBarTrackOverhead;
}
}
throw GuiException("Somehow have a Scroll Bar without a parent.");
}
float ScrollBar::sizeRatio() const {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
if (m_direction == GuiDirection::Vertical) {
return scrollArea->contentSize()[1] / (float)(scrollArea->areaSize()[1]);
} else {
return scrollArea->contentSize()[0] / (float)(scrollArea->areaSize()[0]);
}
}
throw GuiException("Somehow have a Scroll Bar without a parent.");
}
float ScrollBar::scrollRatio() const {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
if (m_direction == GuiDirection::Vertical) {
if (scrollArea->maxScrollPosition()[1] == 0)
return 0;
return scrollArea->scrollOffset()[1] / (float)(scrollArea->maxScrollPosition()[1]);
} else {
if (scrollArea->maxScrollPosition()[0] == 0)
return 0;
return scrollArea->scrollOffset()[0] / (float)(scrollArea->maxScrollPosition()[0]);
}
}
return 0;
}
ButtonWidgetPtr ScrollBar::forwardButton() const {
return m_forward;
}
ButtonWidgetPtr ScrollBar::backwardButton() const {
return m_backward;
}
ScrollThumbPtr ScrollBar::thumb() const {
return m_thumb;
}
void ScrollBar::drawChildren() {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
float ratio = sizeRatio();
if (ratio < 1)
ratio = 1;
int innerSize = (int)max(0.0f, ceil(trackSize() / ratio));
int offsetBegin = (int)ceil((trackSize() - innerSize) * scrollRatio());
innerSize += ScrollThumbOverhead;
if (m_direction == GuiDirection::Vertical) {
if (scrollArea->horizontalScroll()) {
m_forward->setPosition(m_parent->size() - Vec2I(ScrollAreaBorder, ScrollButtonStackSize));
m_backward->setPosition({m_parent->size()[0] - ScrollAreaBorder, ScrollAreaBorder});
m_thumb->setPosition({m_parent->size()[0] - ScrollAreaBorder, ScrollAreaBorder + ScrollButtonStackSize + offsetBegin});
} else {
m_forward->setPosition(m_parent->size() - Vec2I(ScrollAreaBorder, ScrollButtonStackSize));
m_backward->setPosition({m_parent->size()[0] - ScrollAreaBorder, 0});
m_thumb->setPosition({m_parent->size()[0] - ScrollAreaBorder, ScrollButtonStackSize + offsetBegin});
}
m_thumb->setSize(Vec2I(m_thumb->baseSize()[0], innerSize));
} else {
m_forward->setPosition({m_parent->size()[0] - ScrollButtonStackSize, 0});
m_backward->setPosition({0, 0});
m_thumb->setPosition({ScrollButtonStackSize + offsetBegin, 0});
m_thumb->setSize(Vec2I(innerSize, m_thumb->baseSize()[1]));
}
for (auto child : m_members) {
child->render(m_drawingArea);
}
}
}
Vec2I ScrollBar::offsetFromThumbPosition(Vec2I const& thumbPosition) const {
auto scrollArea = convert<ScrollArea>(m_parent);
if (m_direction == GuiDirection::Vertical) {
int scrollSpan = trackSize() - m_thumb->size()[1];
int scrollOffset = clamp((thumbPosition[1] - ScrollButtonStackSize), 0, scrollSpan);
float scrollRatio = (float)scrollOffset / (float)scrollSpan;
return {scrollArea->scrollOffset()[0], ceil(scrollArea->maxScrollPosition()[1] * scrollRatio)};
} else {
int scrollSpan = trackSize() - m_thumb->size()[0];
int scrollOffset = clamp((thumbPosition[0] - ScrollButtonStackSize), 0, scrollSpan);
float scrollRatio = (float)scrollOffset / (float)scrollSpan;
return {(int)ceil(scrollArea->maxScrollPosition()[0] * scrollRatio), scrollArea->scrollOffset()[1]};
}
}
ScrollArea::ScrollArea() {
auto assets = Root::singleton().assets();
m_buttonAdvance = assets->json("/interface.config:scrollArea.buttonAdvance").toInt();
m_advanceLimiter = Time::monotonicMilliseconds();
WidgetCallbackFunc vAdvance = [this](Widget*) { scrollAreaBy({0, advanceFactorHelper()}); };
WidgetCallbackFunc vRetreat = [this](Widget*) { scrollAreaBy({0, -advanceFactorHelper()}); };
WidgetCallbackFunc hAdvance = [this](Widget*) { scrollAreaBy({advanceFactorHelper(), 0}); };
WidgetCallbackFunc hRetreat = [this](Widget*) { scrollAreaBy({-advanceFactorHelper(), 0}); };
m_vBar = make_shared<ScrollBar>(GuiDirection::Vertical, vAdvance, vRetreat);
m_hBar = make_shared<ScrollBar>(GuiDirection::Horizontal, hAdvance, hRetreat);
m_dragActive = false;
addChild("vScrollBar", m_vBar);
addChild("hScrollBar", m_hBar);
m_horizontalScroll = false;
m_verticalScroll = true;
}
void ScrollArea::setButtonImages(Json const& images) {
m_vBar->setButtonImages(images);
m_hBar->setButtonImages(images);
}
void ScrollArea::setThumbImages(Json const& images) {
m_vBar->thumb()->setImages(images);
m_hBar->thumb()->setImages(images);
}
RectI ScrollArea::contentBoundRect() const {
RectI res = RectI::null();
for (auto child : m_members) {
if (child == m_vBar || child == m_hBar) // scroll bars don't count
continue;
if (!child->active()) // neither do hidden members
continue;
res.combine(child->relativeBoundRect());
}
res.setMax({res.max()[0], res.max()[1] + 1});
return res;
}
Vec2I ScrollArea::contentSize() const {
return contentBoundRect().size();
}
Vec2I ScrollArea::areaSize() const {
Vec2I s = size();
if (horizontalScroll())
s[1] -= ScrollAreaBorder;
if (verticalScroll())
s[0] -= ScrollAreaBorder;
return s;
}
void ScrollArea::scrollAreaBy(Vec2I const& offset) {
m_scrollOffset += offset;
}
Vec2I ScrollArea::scrollOffset() const {
return m_scrollOffset;
}
bool ScrollArea::horizontalScroll() const {
return m_horizontalScroll;
}
void ScrollArea::setHorizontalScroll(bool horizontal) {
m_horizontalScroll = horizontal;
}
bool ScrollArea::verticalScroll() const {
return m_verticalScroll;
}
void ScrollArea::setVerticalScroll(bool vertical) {
m_verticalScroll = vertical;
}
bool ScrollArea::sendEvent(InputEvent const& event) {
if (!m_visible)
return false;
if (m_dragActive) {
if (event.is<MouseButtonUpEvent>()) {
blur();
m_dragActive = false;
m_vBar->thumb()->setPressed(false);
m_hBar->thumb()->setPressed(false);
return true;
} else if (event.is<MouseMoveEvent>()) {
auto thumbDragPosition = *context()->mousePosition(event) - screenPosition() - m_dragOffset;
if (m_dragDirection == GuiDirection::Vertical) {
m_scrollOffset = m_vBar->offsetFromThumbPosition(thumbDragPosition);
} else {
m_scrollOffset = m_hBar->offsetFromThumbPosition(thumbDragPosition);
}
return true;
}
}
auto mousePos = context()->mousePosition(event);
if (mousePos && !inMember(*mousePos))
return false;
if (event.is<MouseButtonDownEvent>()) {
if (m_vBar->thumb()->inMember(*mousePos)) {
focus();
m_dragOffset = *mousePos - screenPosition() - m_vBar->thumb()->position();
m_dragDirection = GuiDirection::Vertical;
m_dragActive = true;
m_vBar->thumb()->setPressed(true);
return true;
} else if (m_hBar->thumb()->inMember(*mousePos)) {
focus();
m_dragOffset = *mousePos - screenPosition() - m_hBar->thumb()->position();
m_dragDirection = GuiDirection::Horizontal;
m_dragActive = true;
m_hBar->thumb()->setPressed(true);
return true;
}
}
if (Widget::sendEvent(event))
return true;
if (auto mouseWheel = event.ptr<MouseWheelEvent>()) {
if (mouseWheel->mouseWheel == MouseWheel::Up)
scrollAreaBy({0, m_buttonAdvance * 3});
else
scrollAreaBy({0, -m_buttonAdvance * 3});
return true;
}
return true;
}
void ScrollArea::update() {
if (!m_visible)
return;
auto maxScroll = maxScrollPosition();
// keep vertical scroll bars same distance from the *top* on resize
if (m_verticalScroll && maxScroll != m_lastMaxScroll)
m_scrollOffset += (maxScroll - m_lastMaxScroll);
m_scrollOffset = m_scrollOffset.piecewiseClamp(Vec2I(), maxScroll);
m_lastMaxScroll = maxScroll;
}
Vec2I ScrollArea::maxScrollPosition() const {
return Vec2I().piecewiseMax(contentSize() - size());
}
void ScrollArea::drawChildren() {
RectI innerDrawingArea = m_drawingArea;
if (horizontalScroll())
innerDrawingArea.setYMin(ScrollAreaBorder);
if (verticalScroll())
innerDrawingArea.setXMax(innerDrawingArea.xMax() - ScrollAreaBorder);
auto contentBoundRect = ScrollArea::contentBoundRect();
auto contentOffset = contentBoundRect.min();
auto contentSize = contentBoundRect.size();
auto offset = contentOffset + scrollOffset();
auto areaSize = ScrollArea::areaSize();
if (contentSize[1] < areaSize[1])
offset[1] = offset[1] - (areaSize[1] - contentSize[1]);
for (auto child : m_members) {
if (child == m_vBar || child == m_hBar)
continue;
child->setDrawingOffset(-offset);
child->render(innerDrawingArea);
}
if (m_horizontalScroll)
m_hBar->render(m_drawingArea);
if (m_verticalScroll)
m_vBar->render(m_drawingArea);
}
int ScrollArea::advanceFactorHelper() {
auto t = Time::monotonicMilliseconds() - m_advanceLimiter;
m_advanceLimiter = Time::monotonicMilliseconds();
if ((t > ScrollAdvanceTimer) || (t < 0))
t = ScrollAdvanceTimer;
return (int)std::ceil((m_buttonAdvance * t) / (float)ScrollAdvanceTimer);
}
}