Something went wrong. Try again.
art software for the rest of us
drawing paint image sdl art
Something went wrong. Try again.
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340#include "ScrollArea.h"#include "../UserInterface/Primitives.h"#include <algorithm>#include <cmath>#include <limits>
namespace UI {namespace {constexpr float MaxOverscroll = 64.0f;constexpr float ScrollFriction = 5.0f;constexpr float OverscrollSpring = 180.0f;constexpr float OverscrollDamping = 20.0f;constexpr float SnapSpring = 360.0f;constexpr float SnapDamping = 20.0f;
}float ScrollArea::updateScrollAxis(float position, float& velocity, float minPosition, float maxPosition, float deltaSeconds) {const float boundary = std::clamp(position, minPosition, maxPosition); const bool inOverscroll = (position < minPosition || position > maxPosition);
if (inOverscroll) { velocity += (boundary - position) * OverscrollSpring * deltaSeconds; velocity *= std::exp(-OverscrollDamping * deltaSeconds); } else if (pageDivisions > 0) { float totalRange = maxPosition - minPosition; float pageSize = totalRange / static_cast<float>(pageDivisions);
// 1. Where are we right now? float relativePos = position - minPosition; float currentTargetPage = std::round(relativePos / pageSize);
// 2. Select target page purely based on swipe direction (Velocity) // If moving right/down significantly, target next page. If left/up, previous page. float targetPage = currentTargetPage; if (velocity > 150.0f) { targetPage = std::floor(relativePos / pageSize) + 1.0f; } else if (velocity < -150.0f) { targetPage = std::ceil(relativePos / pageSize) - 1.0f; }
// Keep page within valid limits targetPage = std::clamp(targetPage, 0.0f, static_cast<float>(pageDivisions)); float targetPosition = minPosition + (targetPage * pageSize);
// 3. CAP TERMINAL VELOCITY IN THE DIRECTION OF THE SNAP // Prevents a massive swipe from turning into a rocket launch. float distanceToTarget = targetPosition - position; if (distanceToTarget > 0.0f) { velocity = std::min(velocity, distanceToTarget * 8.0f); // Cap positive speed } else if (distanceToTarget < 0.0f) { velocity = std::max(velocity, distanceToTarget * 8.0f); // Cap negative speed }
// 4. Smoothly pull into the page target velocity += distanceToTarget * SnapSpring * deltaSeconds; velocity *= std::exp(-SnapDamping * deltaSeconds); } else { velocity *= std::exp(-ScrollFriction * deltaSeconds); }
if (std::abs(velocity) < 1.0f) { velocity = 0.0f; }
return std::clamp( position + velocity * deltaSeconds, minPosition - MaxOverscroll, maxPosition + MaxOverscroll );}
void ScrollArea::Measure(Size availableSpace) { const bool horizontal = (scrollingDirections & OverflowFlags::Horizontal) != OverflowFlags::None; const bool vertical = (scrollingDirections & OverflowFlags::Vertical) != OverflowFlags::None; const float unconstrained = std::numeric_limits<float>::max() / 4.0f; Size childAvailable{horizontal ? unconstrained : availableSpace.width, vertical ? unconstrained : availableSpace.height};
float childWidth = 0.0f; float childHeight = 0.0f; for (auto& child : children) { if (!child->IsVisible()) { continue; } child->Measure(childAvailable); childWidth = std::max(childWidth, child->GetDesiredSize().width); childHeight = std::max(childHeight, child->GetDesiredSize().height); }
desiredSize.width = std::clamp(horizontal ? availableSpace.width : childWidth + padding.HorizontalTotal() + margin.HorizontalTotal(), minSize.width, maxSize.width); desiredSize.height = std::clamp(vertical ? availableSpace.height : childHeight + padding.VerticalTotal() + margin.VerticalTotal(), minSize.height, maxSize.height);}
void ScrollArea::Arrange(Rect finalBounds) { ArrangeCore(finalBounds); Rect contentBounds{padding.Left, padding.Top, std::max(0.0f, localBounds.Width - padding.HorizontalTotal()), std::max(0.0f, localBounds.Height - padding.VerticalTotal())}; maxScrollX = 0.0f; maxScrollY = 0.0f; for (auto& child : children) { if (!child->IsVisible()) { continue; } Size desired = child->GetDesiredSize(); maxScrollX = std::max(maxScrollX, desired.width - contentBounds.Width); maxScrollY = std::max(maxScrollY, desired.height - contentBounds.Height); } // apply setpage if early setpage if (pendingPage.has_value() && pageDivisions > 0 && maxScrollX > 0.0f) { int page = pendingPage.value(); float pageSize = maxScrollX / static_cast<float>(pageDivisions); scrollPosition.x = -std::clamp(page * pageSize, 0.0f, maxScrollX); pendingPage.reset(); }
// Allow the overscroll zone to survive layout re-passes; the spring in // UpdateScrollAxis will pull the position back to [−maxScroll, 0] once // the user releases. Hard-clamping here was the cause of the snap-back. scrollPosition.x = std::clamp(scrollPosition.x, -maxScrollX - MaxOverscroll, MaxOverscroll); scrollPosition.y = std::clamp(scrollPosition.y, -maxScrollY - MaxOverscroll, MaxOverscroll);
for (auto& child : children) { if (child->IsVisible()) { child->SetScrollOffset({-scrollPosition.x, -scrollPosition.y}); } }
for (auto& child : children) { if (!child->IsVisible()) { continue; }
Size childDesired = child->GetDesiredSize(); Thickness childMargin = child->GetMargin();
float availW = std::max(0.0f, contentBounds.Width - childMargin.HorizontalTotal()); float availH = std::max(0.0f, contentBounds.Height - childMargin.VerticalTotal()); float naturalW = std::max(0.0f, childDesired.width - childMargin.HorizontalTotal()); float naturalH = std::max(0.0f, childDesired.height - childMargin.VerticalTotal());
auto horiz = CalculateAxisAlignment( child->GetHorizontalAlignment(), (scrollingDirections & OverflowFlags::Horizontal) != OverflowFlags::None ? naturalW : availW, naturalW, childMargin.Left ); auto vert = CalculateAxisAlignment( child->GetVerticalAlignment(), (scrollingDirections & OverflowFlags::Vertical) != OverflowFlags::None ? naturalH : availH, naturalH, childMargin.Top );
child->Arrange(Rect( contentBounds.X + horiz.offset, contentBounds.Y + vert.offset, horiz.size, vert.size )); }
ClearDirty(DirtyFlags::Layout);}
void ScrollArea::Draw() { if (!visible) { return; }
Point previousTranslation = Primitives::GetTranslation(); // Contents include the widget's padding; only the space outside the // widget (including its margin) should be excluded from rendering. Rect clipBounds = globalBounds;
Primitives::SetTranslation({ previousTranslation.x + scrollPosition.x, previousTranslation.y + scrollPosition.y }); if (clipContents) { Primitives::PushClip(clipBounds); }
for (auto& child : children) { if (child->IsVisible()) { child->Draw(); } }
Primitives::SetTranslation(previousTranslation); if (clipContents) { Primitives::PopClip(); } ClearDirty(DirtyFlags::Render);}
void ScrollArea::Update(float deltaSeconds) { // Keep updating descendants, even while this scroll area is coasting. Widget::Update(deltaSeconds);
if (dragging || deltaSeconds <= 0.0f) { return; }
const float oldX = scrollPosition.x; const float oldY = scrollPosition.y;
if ((scrollingDirections & OverflowFlags::Horizontal) != OverflowFlags::None) { scrollPosition.x = updateScrollAxis(scrollPosition.x, velocity.x, -maxScrollX, 0.0f, deltaSeconds); } if ((scrollingDirections & OverflowFlags::Vertical) != OverflowFlags::None) { scrollPosition.y = updateScrollAxis(scrollPosition.y, velocity.y, -maxScrollY, 0.0f, deltaSeconds); }
if (scrollPosition.x != oldX || scrollPosition.y != oldY) { SetScrollOffset({-scrollPosition.x, -scrollPosition.y}); MarkDirty(DirtyFlags::Render); }}
void ScrollArea::SetScrollOffset(Point offset) { for (auto& child : children) { if (child->IsVisible()) { child->SetScrollOffset(offset); } }}
void ScrollArea::SetPage(int page) { if (pageDivisions <= 0) { return; } if (maxScrollX > 0.0f) { float pageSize = maxScrollX / static_cast<float>(pageDivisions); scrollPosition.x = -std::clamp(page * pageSize, 0.0f, maxScrollX); SetScrollOffset({-scrollPosition.x, -scrollPosition.y}); MarkDirty(DirtyFlags::Render); } else { pendingPage = page; }}
Widget* ScrollArea::HitTest(Point screenPoint) { if (!visible || !enabled || !globalBounds.Contains(screenPoint)) { return nullptr; }
Rect contentBounds{ globalBounds.X + padding.Left, globalBounds.Y + padding.Top, std::max(0.0f, globalBounds.Width - padding.HorizontalTotal()), std::max(0.0f, globalBounds.Height - padding.VerticalTotal()) }; if (!contentBounds.Contains(screenPoint)) { return this; }
Point contentPoint{ screenPoint.x - scrollPosition.x, screenPoint.y - scrollPosition.y }; for (auto it = children.rbegin(); it != children.rend(); ++it) { Widget* hit = (*it)->HitTest(contentPoint); if (hit) { return hit; } }
return this;}
bool ScrollArea::WantsPointerCapture(const std::shared_ptr<PointerStream>& stream) { const auto event = stream->PeekLatestInput(); if (event.actDirection == PointerDirection::Down) { if (!globalBounds.Contains(event.screenX, event.screenY)) { return false; } dragging = true; movedDuringDrag = false; activePointerId = event.pointerId; lastPointerX = event.screenX; lastPointerY = event.screenY; lastPointerTimestampNanos = event.timestampNanos; velocity = {0.0f, 0.0f}; return true; }
if (event.pointerId != activePointerId) { return false; }
if (event.actDirection == PointerDirection::None && dragging) { float deltaX = event.screenX - lastPointerX; float deltaY = event.screenY - lastPointerY; lastPointerX = event.screenX; lastPointerY = event.screenY; float deltaSeconds = 1.0f / 60.0f; if (event.timestampNanos > lastPointerTimestampNanos) { deltaSeconds = static_cast<float>(event.timestampNanos - lastPointerTimestampNanos) / 1.0e9f; } lastPointerTimestampNanos = event.timestampNanos; // Ignore pathological event gaps; the next event still moves the view, // but should not turn a stale drag into a large inertial fling. deltaSeconds = std::clamp(deltaSeconds, 1.0e-4f, 0.1f); if ((scrollingDirections & OverflowFlags::Horizontal) != OverflowFlags::None) { scrollPosition.x = std::clamp( scrollPosition.x + deltaX, -maxScrollX - MaxOverscroll, MaxOverscroll ); velocity.x = deltaX / deltaSeconds; } if ((scrollingDirections & OverflowFlags::Vertical) != OverflowFlags::None) { scrollPosition.y = std::clamp( scrollPosition.y + deltaY, -maxScrollY - MaxOverscroll, MaxOverscroll ); velocity.y = deltaY / deltaSeconds; } movedDuringDrag = movedDuringDrag || deltaX != 0.0f || deltaY != 0.0f; if (movedDuringDrag) { SetScrollOffset({-scrollPosition.x, -scrollPosition.y}); MarkDirty(DirtyFlags::Render); return true; } }
if (event.actDirection == PointerDirection::Up) { bool consumed = dragging && movedDuringDrag; dragging = false; movedDuringDrag = false; return consumed; } return false;}
bool ScrollArea::OnPointerStream(const std::shared_ptr<PointerStream>& stream) { return Widget::OnPointerStream(stream);}
}