| @@ -184,6 +184,18 @@ struct Vec { | |||
| Vec() {} | |||
| Vec(float x, float y) : x(x), y(y) {} | |||
| float get(int index) const { | |||
| return (index == 0) ? x : y; | |||
| } | |||
| float& get(int index) { | |||
| return (index == 0) ? x : y; | |||
| } | |||
| void set(int index, float value) { | |||
| if (index == 0) | |||
| x = value; | |||
| else | |||
| y = value; | |||
| } | |||
| /** Negates the vector. | |||
| Equivalent to a reflection across the `y = -x` line. | |||
| */ | |||
| @@ -9,19 +9,17 @@ namespace ui { | |||
| /** Parent must be a ScrollWidget */ | |||
| struct ScrollBar : widget::OpaqueWidget { | |||
| enum Orientation { | |||
| VERTICAL, | |||
| HORIZONTAL | |||
| }; | |||
| Orientation orientation; | |||
| float offset = 0.0; | |||
| float size = 0.0; | |||
| struct Internal; | |||
| Internal* internal; | |||
| bool vertical = false; | |||
| ScrollBar(); | |||
| ~ScrollBar(); | |||
| void draw(const DrawArgs& args) override; | |||
| void onDragStart(const event::DragStart& e) override; | |||
| void onDragMove(const event::DragMove& e) override; | |||
| void onDragEnd(const event::DragEnd& e) override; | |||
| void onDragMove(const event::DragMove& e) override; | |||
| }; | |||
| @@ -10,10 +10,15 @@ namespace ui { | |||
| /** Handles a container with ScrollBar */ | |||
| struct ScrollWidget : widget::OpaqueWidget { | |||
| struct Internal; | |||
| Internal* internal; | |||
| widget::Widget* container; | |||
| ScrollBar* horizontalScrollBar; | |||
| ScrollBar* verticalScrollBar; | |||
| math::Vec offset; | |||
| math::Rect containerBox; | |||
| ScrollWidget(); | |||
| void scrollTo(math::Rect r); | |||
| @@ -33,23 +33,21 @@ struct Widget { | |||
| virtual ~Widget(); | |||
| math::Rect getBox(); | |||
| void setBox(math::Rect box); | |||
| math::Rect getBox() { | |||
| return box; | |||
| } | |||
| math::Vec getPosition(); | |||
| void setPosition(math::Vec pos); | |||
| math::Vec getPosition() { | |||
| return box.pos; | |||
| } | |||
| math::Vec getSize(); | |||
| void setSize(math::Vec size); | |||
| math::Vec getSize() { | |||
| return box.size; | |||
| bool isVisible(); | |||
| void setVisible(bool visible); | |||
| void show() { | |||
| setVisible(true); | |||
| } | |||
| void show(); | |||
| void hide(); | |||
| bool isVisible() { | |||
| return visible; | |||
| void hide() { | |||
| setVisible(false); | |||
| } | |||
| void requestDelete(); | |||
| virtual math::Rect getChildrenBoundingBox(); | |||
| @@ -8,40 +8,63 @@ namespace rack { | |||
| namespace ui { | |||
| // Internal not currently used | |||
| ScrollBar::ScrollBar() { | |||
| box.size = math::Vec(BND_SCROLLBAR_WIDTH, BND_SCROLLBAR_HEIGHT); | |||
| } | |||
| ScrollBar::~ScrollBar() { | |||
| } | |||
| void ScrollBar::draw(const DrawArgs& args) { | |||
| ScrollWidget* sw = dynamic_cast<ScrollWidget*>(parent); | |||
| assert(sw); | |||
| BNDwidgetState state = BND_DEFAULT; | |||
| if (APP->event->hoveredWidget == this) | |||
| if (APP->event->getHoveredWidget() == this) | |||
| state = BND_HOVER; | |||
| if (APP->event->draggedWidget == this) | |||
| if (APP->event->getDraggedWidget() == this) | |||
| state = BND_ACTIVE; | |||
| bndScrollBar(args.vg, 0.0, 0.0, box.size.x, box.size.y, state, offset, size); | |||
| float offsetBound = sw->containerBox.size.get(vertical) - sw->box.size.get(vertical); | |||
| // The handle position relative to the scrollbar. [0, 1] | |||
| float scrollBarOffset = (sw->offset.get(vertical) - sw->containerBox.pos.get(vertical)) / offsetBound; | |||
| // The handle size relative to the scrollbar. [0, 1] | |||
| float scrollBarSize = sw->box.size.get(vertical) / sw->containerBox.size.get(vertical); | |||
| bndScrollBar(args.vg, 0.0, 0.0, box.size.x, box.size.y, state, scrollBarOffset, scrollBarSize); | |||
| } | |||
| void ScrollBar::onDragStart(const event::DragStart& e) { | |||
| if (e.button != GLFW_MOUSE_BUTTON_LEFT) | |||
| return; | |||
| if (e.button == GLFW_MOUSE_BUTTON_LEFT) { | |||
| APP->window->cursorLock(); | |||
| } | |||
| } | |||
| APP->window->cursorLock(); | |||
| void ScrollBar::onDragEnd(const event::DragEnd& e) { | |||
| if (e.button == GLFW_MOUSE_BUTTON_LEFT) { | |||
| APP->window->cursorUnlock(); | |||
| } | |||
| } | |||
| void ScrollBar::onDragMove(const event::DragMove& e) { | |||
| const float sensitivity = 1.f; | |||
| ScrollWidget* sw = dynamic_cast<ScrollWidget*>(parent); | |||
| assert(sw); | |||
| ScrollWidget* scrollWidget = dynamic_cast<ScrollWidget*>(parent); | |||
| assert(scrollWidget); | |||
| if (orientation == HORIZONTAL) | |||
| scrollWidget->offset.x += sensitivity * e.mouseDelta.x; | |||
| else | |||
| scrollWidget->offset.y += sensitivity * e.mouseDelta.y; | |||
| } | |||
| // TODO | |||
| // float offsetBound = sw->containerBox.size.get(vertical) - sw->box.size.get(vertical); | |||
| // float scrollBarSize = sw->box.size.get(vertical) / sw->containerBox.size.get(vertical); | |||
| void ScrollBar::onDragEnd(const event::DragEnd& e) { | |||
| APP->window->cursorUnlock(); | |||
| const float sensitivity = 1.f; | |||
| float offsetDelta = e.mouseDelta.get(vertical); | |||
| offsetDelta *= sensitivity; | |||
| sw->offset.get(vertical) += offsetDelta; | |||
| } | |||
| @@ -6,37 +6,43 @@ namespace rack { | |||
| namespace ui { | |||
| // Internal not currently used | |||
| ScrollWidget::ScrollWidget() { | |||
| container = new widget::Widget; | |||
| addChild(container); | |||
| horizontalScrollBar = new ScrollBar; | |||
| horizontalScrollBar->orientation = ScrollBar::HORIZONTAL; | |||
| horizontalScrollBar->visible = false; | |||
| horizontalScrollBar->vertical = false; | |||
| horizontalScrollBar->hide(); | |||
| addChild(horizontalScrollBar); | |||
| verticalScrollBar = new ScrollBar; | |||
| verticalScrollBar->orientation = ScrollBar::VERTICAL; | |||
| verticalScrollBar->visible = false; | |||
| verticalScrollBar->vertical = true; | |||
| verticalScrollBar->hide(); | |||
| addChild(verticalScrollBar); | |||
| } | |||
| void ScrollWidget::scrollTo(math::Rect r) { | |||
| math::Rect bound = math::Rect::fromMinMax(r.getBottomRight().minus(box.size), r.pos); | |||
| offset = offset.clampSafe(bound); | |||
| } | |||
| void ScrollWidget::draw(const DrawArgs& args) { | |||
| nvgScissor(args.vg, RECT_ARGS(args.clipBox)); | |||
| Widget::draw(args); | |||
| nvgResetScissor(args.vg); | |||
| } | |||
| void ScrollWidget::step() { | |||
| Widget::step(); | |||
| // Clamp scroll offset | |||
| math::Rect containerBox = container->getChildrenBoundingBox(); | |||
| containerBox = container->getChildrenBoundingBox(); | |||
| math::Rect offsetBounds = containerBox; | |||
| offsetBounds.size = offsetBounds.size.minus(box.size); | |||
| offset = offset.clamp(offsetBounds); | |||
| @@ -44,25 +50,19 @@ void ScrollWidget::step() { | |||
| // Update the container's position from the offset | |||
| container->box.pos = offset.neg().round(); | |||
| // Update scrollbar offsets and sizes | |||
| math::Vec scrollbarOffset = offset.minus(containerBox.pos).div(offsetBounds.size); | |||
| math::Vec scrollbarSize = box.size.div(containerBox.size); | |||
| horizontalScrollBar->visible = (0.0 < scrollbarSize.x && scrollbarSize.x < 1.0); | |||
| verticalScrollBar->visible = (0.0 < scrollbarSize.y && scrollbarSize.y < 1.0); | |||
| horizontalScrollBar->offset = scrollbarOffset.x; | |||
| verticalScrollBar->offset = scrollbarOffset.y; | |||
| horizontalScrollBar->size = scrollbarSize.x; | |||
| verticalScrollBar->size = scrollbarSize.y; | |||
| // Make scrollbars visible only if there is a positive range to scroll. | |||
| 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->visible ? inner.x : box.size.x; | |||
| verticalScrollBar->box.size.y = horizontalScrollBar->visible ? inner.y : box.size.y; | |||
| horizontalScrollBar->box.size.x = verticalScrollBar->isVisible() ? inner.x : box.size.x; | |||
| verticalScrollBar->box.size.y = horizontalScrollBar->isVisible() ? inner.y : box.size.y; | |||
| } | |||
| void ScrollWidget::onButton(const event::Button& e) { | |||
| OpaqueWidget::onButton(e); | |||
| if (e.isConsumed()) | |||
| @@ -77,12 +77,14 @@ void ScrollWidget::onButton(const event::Button& e) { | |||
| } | |||
| } | |||
| void ScrollWidget::onDragStart(const event::DragStart& e) { | |||
| if (e.button == GLFW_MOUSE_BUTTON_LEFT || e.button == GLFW_MOUSE_BUTTON_MIDDLE) { | |||
| e.consume(this); | |||
| } | |||
| } | |||
| void ScrollWidget::onDragMove(const event::DragMove& e) { | |||
| // Scroll only if the scrollbars are visible | |||
| if (!(horizontalScrollBar->visible || verticalScrollBar->visible)) | |||
| @@ -91,6 +93,7 @@ void ScrollWidget::onDragMove(const event::DragMove& e) { | |||
| offset = offset.minus(e.mouseDelta); | |||
| } | |||
| void ScrollWidget::onHoverScroll(const event::HoverScroll& e) { | |||
| OpaqueWidget::onHoverScroll(e); | |||
| if (e.isConsumed()) | |||
| @@ -112,6 +115,7 @@ void ScrollWidget::onHoverScroll(const event::HoverScroll& e) { | |||
| e.consume(this); | |||
| } | |||
| void ScrollWidget::onHoverKey(const event::HoverKey& e) { | |||
| OpaqueWidget::onHoverKey(e); | |||
| if (e.isConsumed()) | |||
| @@ -14,11 +14,23 @@ Widget::~Widget() { | |||
| clearChildren(); | |||
| } | |||
| math::Rect Widget::getBox() { | |||
| return box; | |||
| } | |||
| void Widget::setBox(math::Rect box) { | |||
| setPosition(box.pos); | |||
| setSize(box.size); | |||
| } | |||
| math::Vec Widget::getPosition() { | |||
| return box.pos; | |||
| } | |||
| void Widget::setPosition(math::Vec pos) { | |||
| if (pos.isEqual(box.pos)) | |||
| return; | |||
| @@ -28,6 +40,12 @@ void Widget::setPosition(math::Vec pos) { | |||
| onReposition(eReposition); | |||
| } | |||
| math::Vec Widget::getSize() { | |||
| return box.size; | |||
| } | |||
| void Widget::setSize(math::Vec size) { | |||
| if (size.isEqual(box.size)) | |||
| return; | |||
| @@ -37,28 +55,34 @@ void Widget::setSize(math::Vec size) { | |||
| onResize(eResize); | |||
| } | |||
| void Widget::show() { | |||
| if (visible) | |||
| return; | |||
| visible = true; | |||
| // Trigger Show event | |||
| event::Show eShow; | |||
| onShow(eShow); | |||
| bool Widget::isVisible() { | |||
| return visible; | |||
| } | |||
| void Widget::hide() { | |||
| if (!visible) | |||
| void Widget::setVisible(bool visible) { | |||
| if (visible == this->visible) | |||
| return; | |||
| visible = false; | |||
| // Trigger Hide event | |||
| event::Hide eHide; | |||
| onHide(eHide); | |||
| this->visible = visible; | |||
| if (visible) { | |||
| // Trigger Show event | |||
| event::Show eShow; | |||
| onShow(eShow); | |||
| } | |||
| else { | |||
| // Trigger Hide event | |||
| event::Hide eHide; | |||
| onHide(eHide); | |||
| } | |||
| } | |||
| void Widget::requestDelete() { | |||
| requestedDelete = true; | |||
| } | |||
| math::Rect Widget::getChildrenBoundingBox() { | |||
| math::Vec min = math::Vec(INFINITY, INFINITY); | |||
| math::Vec max = math::Vec(-INFINITY, -INFINITY); | |||
| @@ -71,6 +95,7 @@ math::Rect Widget::getChildrenBoundingBox() { | |||
| return math::Rect::fromMinMax(min, max); | |||
| } | |||
| math::Vec Widget::getRelativeOffset(math::Vec v, Widget* relative) { | |||
| if (this == relative) { | |||
| return v; | |||
| @@ -82,6 +107,7 @@ math::Vec Widget::getRelativeOffset(math::Vec v, Widget* relative) { | |||
| return v; | |||
| } | |||
| math::Rect Widget::getViewport(math::Rect r) { | |||
| math::Rect bound; | |||
| if (parent) { | |||
| @@ -94,6 +120,7 @@ math::Rect Widget::getViewport(math::Rect r) { | |||
| return r.clamp(bound); | |||
| } | |||
| void Widget::addChild(Widget* child) { | |||
| assert(child); | |||
| assert(!child->parent); | |||
| @@ -104,6 +131,7 @@ void Widget::addChild(Widget* child) { | |||
| child->onAdd(eAdd); | |||
| } | |||
| void Widget::addChildBottom(Widget* child) { | |||
| assert(child); | |||
| assert(!child->parent); | |||
| @@ -114,6 +142,7 @@ void Widget::addChildBottom(Widget* child) { | |||
| child->onAdd(eAdd); | |||
| } | |||
| void Widget::removeChild(Widget* child) { | |||
| assert(child); | |||
| // Make sure `this` is the child's parent | |||
| @@ -131,6 +160,7 @@ void Widget::removeChild(Widget* child) { | |||
| child->parent = NULL; | |||
| } | |||
| void Widget::clearChildren() { | |||
| for (Widget* child : children) { | |||
| // Trigger Remove event | |||
| @@ -143,6 +173,7 @@ void Widget::clearChildren() { | |||
| children.clear(); | |||
| } | |||
| void Widget::step() { | |||
| for (auto it = children.begin(); it != children.end();) { | |||
| Widget* child = *it; | |||
| @@ -163,6 +194,7 @@ void Widget::step() { | |||
| } | |||
| } | |||
| void Widget::draw(const DrawArgs& args) { | |||
| // Iterate children | |||
| for (Widget* child : children) { | |||