#include #include namespace rack { namespace ui { struct ScrollWidget::Internal { bool scrolling = false; }; ScrollWidget::ScrollWidget() { internal = new Internal; container = new widget::Widget; addChild(container); horizontalScrollbar = new Scrollbar; horizontalScrollbar->vertical = false; horizontalScrollbar->hide(); addChild(horizontalScrollbar); verticalScrollbar = new Scrollbar; verticalScrollbar->vertical = true; verticalScrollbar->hide(); addChild(verticalScrollbar); } ScrollWidget::~ScrollWidget() { delete internal; } void ScrollWidget::scrollTo(math::Rect r) { math::Rect bound = math::Rect::fromMinMax(r.getBottomRight().minus(box.size), r.pos); offset = offset.clampSafe(bound); } math::Rect ScrollWidget::getContainerOffsetBound() { math::Rect r; r.pos = containerBox.pos; r.size = containerBox.size.minus(box.size); return r; } math::Vec ScrollWidget::getHandleOffset() { return offset.minus(containerBox.pos).div(getContainerOffsetBound().size); } math::Vec ScrollWidget::getHandleSize() { return box.size.div(containerBox.size); } bool ScrollWidget::isScrolling() { return internal->scrolling; } void ScrollWidget::draw(const DrawArgs& args) { nvgScissor(args.vg, RECT_ARGS(args.clipBox)); Widget::draw(args); nvgResetScissor(args.vg); } void ScrollWidget::step() { Widget::step(); // Set containerBox cache containerBox = container->getVisibleChildrenBoundingBox(); // Clamp scroll offset math::Rect offsetBounds = getContainerOffsetBound(); offset = offset.clamp(offsetBounds); // Update the container's position from the offset container->box.pos = offset.neg().round(); // Make scrollbars visible only if there is a positive range to scroll. if (hideScrollbars) { horizontalScrollbar->setVisible(false); verticalScrollbar->setVisible(false); } else { horizontalScrollbar->setVisible(offsetBounds.size.x > 0.f); verticalScrollbar->setVisible(offsetBounds.size.y > 0.f); } // Reposition and resize scroll bars math::Vec inner = box.size.minus(math::Vec(verticalScrollbar->box.size.x, horizontalScrollbar->box.size.y)); horizontalScrollbar->box.pos.y = inner.y; verticalScrollbar->box.pos.x = inner.x; horizontalScrollbar->box.size.x = verticalScrollbar->isVisible() ? inner.x : box.size.x; verticalScrollbar->box.size.y = horizontalScrollbar->isVisible() ? inner.y : box.size.y; } void ScrollWidget::onHover(const HoverEvent& e) { OpaqueWidget::onHover(e); if (!e.mouseDelta.isZero()) { internal->scrolling = false; } } void ScrollWidget::onButton(const ButtonEvent& e) { math::Rect offsetBound = getContainerOffsetBound(); // Check if scrollable if (offsetBound.size.x > 0.f || offsetBound.size.y > 0.f) { // Handle Alt-click before children, since most widgets consume Alt-click without needing to. if (e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == GLFW_MOD_ALT) { e.consume(this); return; } // Might as well handle middle click before children as well. if (e.button == GLFW_MOUSE_BUTTON_MIDDLE) { e.consume(this); return; } } Widget::onButton(e); } void ScrollWidget::onDragStart(const DragStartEvent& e) { e.consume(this); } void ScrollWidget::onDragMove(const DragMoveEvent& e) { math::Vec offsetDelta = e.mouseDelta.div(getAbsoluteZoom()); offset = offset.minus(offsetDelta); } void ScrollWidget::onHoverScroll(const HoverScrollEvent& e) { OpaqueWidget::onHoverScroll(e); if (e.isConsumed()) return; int mods = APP->window->getMods(); // Don't scroll when Ctrl is held because this interferes with RackScrollWidget zooming. if ((mods & RACK_MOD_MASK) & RACK_MOD_CTRL) return; // Check if scrollable math::Rect offsetBound = getContainerOffsetBound(); if (offsetBound.size.x <= 0.f && offsetBound.size.y <= 0.f) return; math::Vec scrollDelta = e.scrollDelta; // Flip coordinates if shift is held // Mac (or GLFW?) already does this for us. #if !defined ARCH_MAC if ((mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) scrollDelta = scrollDelta.flip(); #endif offset = offset.minus(scrollDelta); e.consume(this); internal->scrolling = true; } void ScrollWidget::onHoverKey(const HoverKeyEvent& e) { OpaqueWidget::onHoverKey(e); if (e.isConsumed()) return; // Check if scrollable math::Rect offsetBound = getContainerOffsetBound(); if (offsetBound.size.x <= 0.f && offsetBound.size.y <= 0.f) return; if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) { if (e.key == GLFW_KEY_PAGE_UP && (e.mods & RACK_MOD_MASK) == 0) { offset.y -= box.size.y * 0.5; e.consume(this); } if (e.key == GLFW_KEY_PAGE_UP && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) { offset.x -= box.size.x * 0.5; e.consume(this); } if (e.key == GLFW_KEY_PAGE_DOWN && (e.mods & RACK_MOD_MASK) == 0) { offset.y += box.size.y * 0.5; e.consume(this); } if (e.key == GLFW_KEY_PAGE_DOWN && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) { offset.x += box.size.x * 0.5; e.consume(this); } if (e.key == GLFW_KEY_HOME && (e.mods & RACK_MOD_MASK) == 0) { math::Rect containerBox = container->getVisibleChildrenBoundingBox(); offset.y = containerBox.getTop(); e.consume(this); } if (e.key == GLFW_KEY_HOME && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) { math::Rect containerBox = container->getVisibleChildrenBoundingBox(); offset.x = containerBox.getLeft(); e.consume(this); } if (e.key == GLFW_KEY_END && (e.mods & RACK_MOD_MASK) == 0) { math::Rect containerBox = container->getVisibleChildrenBoundingBox(); offset.y = containerBox.getBottom(); e.consume(this); } if (e.key == GLFW_KEY_END && (e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) { math::Rect containerBox = container->getVisibleChildrenBoundingBox(); offset.x = containerBox.getRight(); e.consume(this); } } } } // namespace ui } // namespace rack