#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(); m_forward->setCallback(forwardFunc); m_forward->setSustainCallbackOnDownHold(true); m_forward->setPressedOffset({0, 0}); m_backward = make_shared(); m_backward->setCallback(backwardFunc); m_backward->setSustainCallbackOnDownHold(true); m_backward->setPressedOffset({0, 0}); m_thumb = make_shared(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(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(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(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(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(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(GuiDirection::Vertical, vAdvance, vRetreat); m_hBar = make_shared(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()) { blur(); m_dragActive = false; m_vBar->thumb()->setPressed(false); m_hBar->thumb()->setPressed(false); return true; } else if (event.is()) { 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()) { 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()) { if (mouseWheel->mouseWheel == MouseWheel::Up) scrollAreaBy({0, m_buttonAdvance * 3}); else scrollAreaBy({0, -m_buttonAdvance * 3}); return true; } return true; } void ScrollArea::update(float) { 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); } }