WIP: ScrollView

This commit is contained in:
Daniil Vinogradov
2025-01-24 22:02:46 +01:00
parent 758c68d3d9
commit c011de5ceb
28 changed files with 1278 additions and 41 deletions
+6
View File
@@ -32,6 +32,7 @@ add_library(UIKit
lib/UIColor.cpp
lib/UIControl.cpp
lib/UIControlGestureRecognizer.cpp
lib/UIEdgeInsets.cpp
lib/UIEvent.cpp
lib/UIFocus.cpp
lib/UIFocusAnimationCoordinator.cpp
@@ -45,6 +46,7 @@ add_library(UIKit
lib/UIPress.cpp
lib/UIPressesEvent.cpp
lib/UIResponder.cpp
lib/UIScrollView.cpp
lib/UITapGestureRecognizer.cpp
lib/UITouch.cpp
lib/UITraitCollection.cpp
@@ -57,6 +59,10 @@ add_library(UIKit
lib/NXAffineTransform.cpp
lib/NXTransform3D.cpp
lib/NXData.cpp
lib/UIScrollViewExtensions/DecelerationTimingParameters.cpp
lib/UIScrollViewExtensions/RubberBand.cpp
lib/UIScrollViewExtensions/SpringTimingParameters.cpp
lib/UIScrollViewExtensions/TimerAnimation.cpp
lib/SkTools/EventTracingPriv.cpp
lib/SkTools/SkDebugfTracer.cpp
)
+4
View File
@@ -75,6 +75,9 @@ public:
void setMask(const std::shared_ptr<CALayer>& mask);
[[nodiscard]] std::shared_ptr<CALayer> mask() const { return _mask; }
void setContentsTemplateMode(bool newValue) { _isContentsTemplate = newValue; }
[[nodiscard]] bool isContentsTemplateMode() { return _isContentsTemplate; }
void setMasksToBounds(bool newValue) { _masksToBounds = newValue; }
[[nodiscard]] bool masksToBounds() { return _masksToBounds; }
@@ -146,6 +149,7 @@ private:
bool _isHidden = false;
bool _needsDisplay = true;
bool _isContentsTemplate = false;
std::shared_ptr<CGImage> _contents;
bool _masksToBounds = false;
+4 -3
View File
@@ -2,6 +2,7 @@
#include <optional>
#include <memory>
#include <utility>
#include <NXData.h>
#include <Geometry.h>
@@ -14,10 +15,10 @@ public:
sk_sp<SkImage> pointee;
// CGImage(NXSize size);
CGImage(const std::shared_ptr<NXData>& sourceData);
explicit CGImage(const std::shared_ptr<NXData>& sourceData);
// CGImage(SDL_Surface* surface);
CGImage(sk_sp<SkImage> image, std::shared_ptr<NXData> sourceData);
CGImage(sk_sp<SkImage> image): CGImage(image, nullptr) {}
explicit CGImage(sk_sp<SkImage> image, std::shared_ptr<NXData> sourceData);
explicit CGImage(sk_sp<SkImage> image): CGImage(std::move(image), nullptr) {}
~CGImage();
[[nodiscard]] NXSize size() const;
+25 -18
View File
@@ -16,17 +16,19 @@ struct NXPoint {
NXPoint(NXFloat x, NXFloat y);
bool operator==(const NXPoint& rhs) const;
bool operator!=(const NXPoint& rhs) const;
NXPoint operator+(const NXPoint& first) const;
NXPoint operator-(const NXPoint& first) const;
NXPoint& operator+=(const NXPoint& rhs);
NXPoint& operator-=(const NXPoint& rhs);
NXPoint operator/(const NXFloat& rhs);
NXPoint operator*(const NXFloat& rhs);
NXPoint operator/(const NXFloat& rhs) const;
NXPoint operator*(const NXFloat& rhs) const;
NXPoint applying(const NXAffineTransform& t) const;
[[nodiscard]] NXPoint applying(const NXAffineTransform& t) const;
[[nodiscard]] NXFloat distanceToSegment(NXPoint v, NXPoint w) const;
bool valid() const;
NXFloat magnitude() const;
[[nodiscard]] bool valid() const;
[[nodiscard]] NXFloat magnitude() const;
static NXPoint zero;
};
@@ -50,7 +52,7 @@ struct NXSize {
NXSize &operator*=(const NXFloat &rhs);
NXSize &operator/=(const NXFloat &rhs);
bool valid() const;
[[nodiscard]] bool valid() const;
// NXSize inset(UIEdgeInsets inset) const;
};
@@ -62,16 +64,16 @@ struct NXRect {
NXRect(NXPoint origin, NXSize size);
NXRect(NXFloat x, NXFloat y, NXFloat width, NXFloat height);
NXFloat width() const;
NXFloat height() const;
[[nodiscard]] NXFloat width() const;
[[nodiscard]] NXFloat height() const;
NXFloat minX() const;
NXFloat midX() const;
NXFloat maxX() const;
[[nodiscard]] NXFloat minX() const;
[[nodiscard]] NXFloat midX() const;
[[nodiscard]] NXFloat maxX() const;
NXFloat minY() const;
NXFloat midY() const;
NXFloat maxY() const;
[[nodiscard]] NXFloat minY() const;
[[nodiscard]] NXFloat midY() const;
[[nodiscard]] NXFloat maxY() const;
void setWidth(NXFloat newValue);
void setHeight(NXFloat newValue);
@@ -84,8 +86,8 @@ struct NXRect {
void setMidY(NXFloat newValue);
void setMaxY(NXFloat newValue);
bool contains(NXPoint point) const;
bool intersects(const NXRect& other) const;
[[nodiscard]] bool contains(NXPoint point) const;
[[nodiscard]] bool intersects(const NXRect& other) const;
NXRect& offsetBy(const NXPoint& offset);
NXRect& offsetBy(const NXFloat& offsetX, const NXFloat& offsetY);
@@ -97,10 +99,15 @@ struct NXRect {
NXRect applying(NXAffineTransform transform);
NXRect applying(NXTransform3D transform);
NXRect intersection(NXRect other) const;
[[nodiscard]] NXRect intersection(NXRect other) const;
bool isNull() const;
[[nodiscard]] bool isNull() const;
static NXRect null;
};
struct Geometry {
static NXFloat rubberBandClamp(NXFloat x, NXFloat coeff, NXFloat dim);
static NXFloat rubberBandClamp(NXFloat x, NXFloat coeff, NXFloat dim, NXFloat limitStart, NXFloat limitEnd);
};
}
+33
View File
@@ -0,0 +1,33 @@
//
// UIEdgeInsets.hpp
// SDLTest
//
// Created by Даниил Виноградов on 12.03.2023.
//
#pragma once
#include <Geometry.h>
namespace NXKit {
struct UIEdgeInsets {
float top;
float left;
float bottom;
float right;
UIEdgeInsets(float top, float left, float bottom, float right);
UIEdgeInsets(): UIEdgeInsets(0, 0, 0, 0) {}
bool operator==(const UIEdgeInsets& rhs) const;
bool operator!=(const UIEdgeInsets& rhs) const;
UIEdgeInsets operator+(const UIEdgeInsets& rhs) const;
UIEdgeInsets operator-(const UIEdgeInsets& rhs) const;
UIEdgeInsets& operator+=(const UIEdgeInsets& rhs);
UIEdgeInsets& operator-=(const UIEdgeInsets& rhs);
static UIEdgeInsets zero;
};
}
+6 -1
View File
@@ -13,11 +13,16 @@ public:
static std::shared_ptr<UIImage> fromPath(const std::string& path);
static std::shared_ptr<UIImage> fromData(const std::shared_ptr<NXData>& data, NXFloat scale = SkiaCtx::main()->getScaleFactor());
std::shared_ptr<CGImage> cgImage() { return _cgImage; }
[[nodiscard]] std::shared_ptr<CGImage> cgImage() const { return _cgImage; }
[[nodiscard]] NXSize size() const { return _size / _scale; }
[[nodiscard]] NXFloat scale() const { return _scale; }
void setRenderModeAsTemplate(bool isTemplate) { _isTemplate = isTemplate; }
private:
friend class UIImageView;
bool _isTemplate = false;
std::shared_ptr<CGImage> _cgImage;
NXSize _size;
NXFloat _scale;
+3 -2
View File
@@ -10,9 +10,10 @@
#include <UIControl.h>
#include <UIImageView.h>
#include <UILabel.h>
#include <UIViewController.h>
#include <UITapGestureRecognizer.h>
#include <UIPanGestureRecognizer.h>
#include <UIScrollView.h>
#include <UITapGestureRecognizer.h>
#include <UIViewController.h>
#include <tools/Logger.hpp>
#include <yoga/Yoga.h>
+106
View File
@@ -0,0 +1,106 @@
//
// UIScrollView.hpp
// SDLTest
//
// Created by Даниил Виноградов on 12.03.2023.
//
#pragma once
#include <UIView.h>
#include <UIPanGestureRecognizer.h>
#include <UIScrollViewExtensions/DecelerationTimingParameters.h>
#include <UIScrollViewExtensions/TimerAnimation.h>
#include <UIScrollViewExtensions/UIScrollViewDecelerationRate.h>
namespace NXKit {
class UIScrollView;
class UIScrollViewDelegate {
public:
virtual void scrollViewWillBeginDragging(std::shared_ptr<UIScrollView> scrollView) {}
virtual void scrollViewDidScroll(std::shared_ptr<UIScrollView> scrollView) {}
virtual void scrollViewDidEndDragging(std::shared_ptr<UIScrollView> scrollView, bool willDecelerate) {}
};
enum class UIScrollViewContentInsetAdjustmentBehavior {
scrollableAxes,
never,
always
};
class UIScrollView: public UIView {
public:
static std::shared_ptr<UIScrollView> init();
std::weak_ptr<UIScrollViewDelegate> delegate;
UIScrollView(NXRect frame = NXRect());
// virtual void addSubview(std::shared_ptr<UIView> view) override;
// bool applyXMLAttribute(std::string name, std::string value) override;
void layoutSubviews() override;
void layoutMarginsDidChange() override;
NXPoint contentOffset() { return bounds().origin; }
void setContentOffset(NXPoint offset, bool animated);
UIEdgeInsets contentInset() { return _contentInset; }
void setContentInset(UIEdgeInsets contentInset) { _contentInset = contentInset; }
bool bounceHorizontally() const { return _bounceHorizontally; }
void setBounceHorizontally(bool bounceHorizontally);
bool bounceVertically() const { return _bounceVertically; }
void setBounceVertically(bool bounceVertically);
UIScrollViewDecelerationRate decelerationRate() const { return _decelerationRate; }
void setDecelerationRate(UIScrollViewDecelerationRate decelerationRate) { _decelerationRate = decelerationRate; }
UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior() { return _contentInsetAdjustmentBehavior; }
void setContentInsetAdjustmentBehavior(UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior);
NXSize contentSize();
// void setContentSize(Size size) { _contentSize = size; }
private:
std::shared_ptr<UIPanGestureRecognizer> _panGestureRecognizer;
bool _isDecelerating = false;
NXPoint weightedAverageVelocity;
NXPoint _initialContentOffset;
UIScrollViewDecelerationRate _decelerationRate = UIScrollViewDecelerationRate::normal;
bool _bounceHorizontally = false;
bool _bounceVertically = false;
bool shouldBounceHorizontally();
bool shouldBounceVertically();
UIScrollViewContentInsetAdjustmentBehavior _contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior::scrollableAxes;
std::shared_ptr<TimerAnimation> _timerAnimation;
UIEdgeInsets _lastLayoutMargins;
UIEdgeInsets _contentInset;
// Size _contentSize;
void onPan();
void onPanGestureStateChanged();
NXPoint visibleContentOffset();
NXPoint getBoundsCheckedContentOffset(NXPoint newContentOffset);
NXRect contentOffsetBounds();
void layoutScrollIndicatorsIfNeeded();
void showScrollIndicators();
void hideScrollIndicators();
void startDeceleratingIfNecessary();
void cancelDeceleratingIfNeccessary();
void cancelDecelerationAnimations();
void bounceWithVelocity(NXPoint velocity);
};
}
@@ -0,0 +1,32 @@
//
// Created by Даниил Виноградов on 24.01.2025.
//
#pragma once
#include <Geometry.h>
#include <Timer.h>
namespace NXKit {
struct DecelerationTimingParameters {
public:
NXPoint initialValue;
NXPoint initialVelocity;
NXFloat decelerationRate;
NXFloat threshold;
DecelerationTimingParameters(NXPoint initialValue, NXPoint initialVelocity, NXFloat decelerationRate, NXFloat threshold):
initialValue(initialValue),
initialVelocity(initialVelocity),
decelerationRate(decelerationRate),
threshold(threshold) { }
[[nodiscard]] NXPoint destination() const;
[[nodiscard]] NXFloat duration() const;
[[nodiscard]] NXPoint valueAt(float time) const;
[[nodiscard]] NXFloat durationTo(NXPoint value) const;
[[nodiscard]] NXPoint velocityAt(NXFloat time) const;
};
}
@@ -0,0 +1,22 @@
//
// Created by Даниил Виноградов on 12.01.2024.
//
#pragma once
#include <Geometry.h>
namespace NXKit {
struct RubberBand {
float coeff;
NXSize dims;
NXRect bounds;
RubberBand(float coeff, NXSize dims, NXRect bounds):
coeff(coeff), dims(dims), bounds(bounds) {}
NXPoint clamp(NXPoint point) const;
};
}
@@ -0,0 +1,44 @@
//
// Created by Даниил Виноградов on 12.01.2024.
//
#pragma once
#include <Geometry.h>
#include <tools/SharedBase.hpp>
namespace NXKit {
struct TimingParameters {
[[nodiscard]] virtual NXFloat duration() const { return 0; }
[[nodiscard]] virtual NXPoint valueAt(NXFloat time) const { return {}; }
};
struct Spring {
NXFloat mass;
NXFloat stiffness;
NXFloat dampingRatio;
Spring(NXFloat mass, NXFloat stiffness, NXFloat dampingRatio):
mass(mass), stiffness(stiffness), dampingRatio(dampingRatio) {}
[[nodiscard]] NXFloat damping() const;
[[nodiscard]] NXFloat beta() const;
[[nodiscard]] NXFloat dampedNaturalFrequency() const;
};
struct SpringTimingParameters: TimingParameters {
Spring spring;
NXPoint displacement;
NXPoint initialVelocity;
NXFloat threshold;
SpringTimingParameters(Spring spring, NXPoint displacement, NXPoint initialVelocity, NXFloat threshold);
[[nodiscard]] NXFloat duration() const override;
[[nodiscard]] NXPoint valueAt(NXFloat time) const override;
private:
std::shared_ptr<TimingParameters> impl;
};
}
@@ -0,0 +1,31 @@
//
// Created by Daniil Vinogradov on 12/01/2024.
//
#pragma once
#include <CADisplayLink.h>
#include <Timer.h>
namespace NXKit {
class TimerAnimation: enable_shared_from_this_pointer_holder {
public:
TimerAnimation(double duration, std::function<void(float, double)> animations, std::function<void(bool)> completion = [](bool _){});
~TimerAnimation();
void invalidate();
private:
CADisplayLink displayLink;
bool _running = true;
Timer firstFrameTimestamp;
double duration;
std::function<void(float, double)> animations;
std::function<void(bool)> completion;
void handleFrame();
};
}
@@ -0,0 +1,39 @@
//
// Created by Daniil Vinogradov on 12/01/2024.
//
#pragma once
namespace NXKit {
class UIScrollViewDecelerationRate {
public:
enum Value {
normal,
fast
};
UIScrollViewDecelerationRate() = default;
constexpr UIScrollViewDecelerationRate(Value value) : value(value) { }
// Allow switch and comparisons.
constexpr operator Value() const { return value; }
// Prevent usage: if(fruit)
explicit operator bool() const = delete;
[[nodiscard]] constexpr float rawValue() const {
switch (value) {
case normal: {
return 0.998f;
}
case fast: {
return 0.99f;
}
}
}
private:
Value value;
};
}
+37 -5
View File
@@ -2,6 +2,7 @@
#include "CALayer.h"
#include <YGLayout.h>
#include <UIEdgeInsets.h>
#include <UITraitEnvironment.h>
#include <UIViewContentMode.h>
#include <UIResponder.h>
@@ -34,7 +35,7 @@ public:
void setAlpha(NXFloat alpha) { _layer->setOpacity(alpha); }
[[nodiscard]] NXFloat alpha() const { return _layer->opacity(); }
void setHidden(bool hidden) { _layer->setHidden(hidden); }
void setHidden(bool hidden);
[[nodiscard]] bool isHidden() const { return _layer->isHidden(); }
void setClipsToBounds(bool clipsToBounds) { _layer->setMasksToBounds(clipsToBounds); }
@@ -43,7 +44,7 @@ public:
void setTransform(NXAffineTransform transform) { _layer->setAffineTransform(transform); }
[[nodiscard]] NXAffineTransform transform() const { return _layer->affineTransform(); }
void setBackgroundColor(std::optional<UIColor> backbroundColor) { _layer->setBackgroundColor(backbroundColor); }
void setBackgroundColor(const std::optional<UIColor>& backbroundColor) { _layer->setBackgroundColor(backbroundColor); }
[[nodiscard]] std::optional<UIColor> backgroundColor() const { return _layer->backgroundColor(); }
void setTintColor(std::optional<UIColor> tintColor);
@@ -62,8 +63,8 @@ public:
void addGestureRecognizer(const std::shared_ptr<UIGestureRecognizer>& gestureRecognizer);
[[nodiscard]] std::vector<std::shared_ptr<UIGestureRecognizer>>* gestureRecognizers() { return &_gestureRecognizers; }
void addSubview(std::shared_ptr<UIView> view);
void insertSubviewAt(std::shared_ptr<UIView> view, int index);
void addSubview(const std::shared_ptr<UIView>& view);
void insertSubviewAt(const std::shared_ptr<UIView>& view, int index);
void insertSubviewBelow(const std::shared_ptr<UIView>& view, const std::shared_ptr<UIView>& belowSubview);
void removeFromSuperview();
@@ -77,6 +78,20 @@ public:
void traitCollectionDidChange(std::shared_ptr<UITraitCollection> previousTraitCollection) override;
// Layout
UIEdgeInsets layoutMargins();
void setLayoutMargins(UIEdgeInsets layoutMargins);
UIEdgeInsets safeAreaInsets() { return _safeAreaInsets; }
virtual void safeAreaInsetsDidChange() {}
virtual void layoutMarginsDidChange() {}
bool insetsLayoutMarginsFromSafeArea() const { return _insetsLayoutMarginsFromSafeArea; }
void setInsetsLayoutMarginsFromSafeArea(bool insetsLayoutMarginsFromSafeArea);
bool preservesSuperviewLayoutMargins() const { return _preservesSuperviewLayoutMargins; }
void setPreservesSuperviewLayoutMargins(bool preservesSuperviewLayoutMargins);
void setNeedsDisplay() { _needsDisplay = true; }
void setNeedsLayout();// { setNeedsDisplay(); _needsLayout = true; }
@@ -135,7 +150,7 @@ public:
void updateCurrentEnvironment() override;
virtual void draw() {}
virtual void display(std::shared_ptr<CALayer> layer) override;
void display(std::shared_ptr<CALayer> layer) override;
private:
friend class UIViewController;
friend class UIFocusSystem;
@@ -155,8 +170,25 @@ private:
std::optional<UIColor> _tintColor;
UIEdgeInsets _layoutMargins;
UIEdgeInsets _calculatedLayoutMargins;
UIEdgeInsets _safeAreaInsets;
bool _insetsLayoutMarginsFromSafeArea = true;
bool _preservesSuperviewLayoutMargins = false;
void setSafeAreaInsets(UIEdgeInsets safeAreaInsets);
void updateSafeAreaInsetsInChilds();
void updateSafeAreaInsetsIfNeeded();
void updateSafeAreaInsets();
void updateLayoutMarginIfNeeded();
void updateLayoutMargin();
void setNeedsUpdateSafeAreaInsets() { _needsUpdateSafeAreaInsets = true; }
void setNeedsUpdateLayoutMargins() { _needsUpdateLayoutMargins = true; }
bool _needsLayout = true;
bool _needsDisplay = true;
bool _needsUpdateSafeAreaInsets = true;
bool _needsUpdateLayoutMargins = true;
static void animateIfNeeded(Timer currentTime);
+5 -5
View File
@@ -34,10 +34,10 @@ public:
virtual void didMoveToParent(std::shared_ptr<UIViewController> parent);
void removeFromParent();
// UIEdgeInsets additionalSafeAreaInsets() { return _additionalSafeAreaInsets; }
// void setAdditionalSafeAreaInsets(UIEdgeInsets additionalSafeAreaInsets);
UIEdgeInsets additionalSafeAreaInsets() { return _additionalSafeAreaInsets; }
void setAdditionalSafeAreaInsets(UIEdgeInsets additionalSafeAreaInsets);
// UIEdgeInsets systemMinimumLayoutMargins() { return _systemMinimumLayoutMargins; }
UIEdgeInsets systemMinimumLayoutMargins() { return _systemMinimumLayoutMargins; }
bool viewRespectsSystemMinimumLayoutMargins() { return _viewRespectsSystemMinimumLayoutMargins; }
void setViewRespectsSystemMinimumLayoutMargins(bool viewRespectsSystemMinimumLayoutMargins);
@@ -59,8 +59,8 @@ private:
std::shared_ptr<UIView> _view;
std::weak_ptr<UIViewController> _parent;
std::vector<std::shared_ptr<UIViewController>> _children;
// UIEdgeInsets _additionalSafeAreaInsets;
// UIEdgeInsets _systemMinimumLayoutMargins = UIEdgeInsets(0, 16, 0, 16);
UIEdgeInsets _additionalSafeAreaInsets;
UIEdgeInsets _systemMinimumLayoutMargins = UIEdgeInsets(0, 16, 0, 16);
bool _viewRespectsSystemMinimumLayoutMargins = true;
float _animationTime = 0.5;
+8 -1
View File
@@ -241,12 +241,19 @@ void CALayer::skiaRender(SkCanvas* canvas) {
canvas->save();
canvas->translate(x, y);
SkPaint imgPaint;
imgPaint.setAntiAlias(true);
if (_isContentsTemplate) {
imgPaint.setColorFilter(SkColorFilters::Blend(UIColor::tint.raw(), SkBlendMode::kSrcIn));
}
canvas->drawImageRect(_contents->pointee, {
float(0),
float(0),
float(width),
float(height)
}, SkSamplingOptions(), nullptr);
}, SkSamplingOptions(), &imgPaint);
// Contents matrix save 4 // restore
canvas->restore();
+48 -2
View File
@@ -37,6 +37,10 @@ bool NXPoint::operator==(const NXPoint& rhs) const {
return this->x == rhs.x && this->y == rhs.y;
}
bool NXPoint::operator!=(const NXPoint& rhs) const {
return !(*this == rhs);
}
NXPoint NXPoint::operator+(const NXPoint& first) const {
return {x + first.x, y + first.y};
}
@@ -57,14 +61,14 @@ NXPoint& NXPoint::operator-=(const NXPoint& rhs) {
return *this;
}
NXPoint NXPoint::operator/(const NXFloat& rhs) {
NXPoint NXPoint::operator/(const NXFloat& rhs) const {
auto res = *this;
res.x /= rhs;
res.y /= rhs;
return res;
}
NXPoint NXPoint::operator*(const NXFloat& rhs) {
NXPoint NXPoint::operator*(const NXFloat& rhs) const {
auto res = *this;
res.x *= rhs;
res.y *= rhs;
@@ -78,6 +82,36 @@ NXPoint NXPoint::applying(const NXAffineTransform& t) const {
};
}
float NXPoint::distanceToSegment(NXPoint v, NXPoint w) const {
auto pv_dx = x - v.x;
auto pv_dy = y - v.y;
auto wv_dx = w.x - v.x;
auto wv_dy = w.y - v.y;
auto dot = pv_dx * wv_dx + pv_dy * wv_dy;
auto len_sq = wv_dx * wv_dx + wv_dy * wv_dy;
auto param = dot / len_sq;
float int_x, int_y; /* intersection of normal to vw that goes through p */
if (param < 0 || (v.x == w.x && v.y == w.y)) {
int_x = v.x;
int_y = v.y;
} else if (param > 1) {
int_x = w.x;
int_y = w.y;
} else {
int_x = v.x + param * wv_dx;
int_y = v.y + param * wv_dy;
}
/* Components of normal */
auto dx = x - int_x;
auto dy = y - int_y;
return sqrt(dx * dx + dy * dy);
}
bool NXPoint::valid() const {
return !std::isnan(this->x) && !std::isnan(this->y);
}
@@ -288,3 +322,15 @@ bool NXRect::isNull() const {
}
NXRect NXRect::null = NXRect(INFINITY, INFINITY, 0, 0);
NXFloat Geometry::rubberBandClamp(NXFloat x, NXFloat coeff, NXFloat dim) {
return (1.0f - (1.0f / ((x * coeff / dim) + 1.0f))) * dim;
}
NXFloat Geometry::rubberBandClamp(NXFloat x, NXFloat coeff, NXFloat dim, NXFloat limitStart, NXFloat limitEnd) {
auto clampedX = fminf(fmaxf(x, limitStart), limitEnd);
auto diff = abs(x - clampedX);
float sign = clampedX > x ? -1 : 1;
return clampedX + sign * rubberBandClamp(diff, coeff, dim);
}
+4
View File
@@ -68,18 +68,22 @@ void UIButton::applyStyle(UIButtonStyle style) {
case UIButtonStyle::plain:
setBackgroundColor(UIColor::clear);
_titleLabel->setTextColor(UIColor::tint);
_imageView->setTintColor(UIColor::tint);
break;
case UIButtonStyle::gray:
setBackgroundColor(UIColor::systemGray);
_titleLabel->setTextColor(UIColor::tint);
_imageView->setTintColor(UIColor::tint);
break;
case UIButtonStyle::tinted:
setBackgroundColor(UIColor::tint.withAlphaComponent(0.2f));
_titleLabel->setTextColor(UIColor::tint);
_imageView->setTintColor(UIColor::tint);
break;
case UIButtonStyle::filled:
setBackgroundColor(UIColor::tint);
_titleLabel->setTextColor(UIColor::white);
_imageView->setTintColor(UIColor::white);
break;
}
}
+53
View File
@@ -0,0 +1,53 @@
//
// UIEdgeInsets.cpp
// SDLTest
//
// Created by Даниил Виноградов on 12.03.2023.
//
#include <UIEdgeInsets.h>
namespace NXKit {
UIEdgeInsets::UIEdgeInsets(float top, float left, float bottom, float right):
top(top), left(left), bottom(bottom), right(right)
{ }
bool UIEdgeInsets::operator==(const UIEdgeInsets& rhs) const {
return top == rhs.top &&
left == rhs.left &&
bottom == rhs.bottom &&
right == rhs.right;
}
bool UIEdgeInsets::operator!=(const UIEdgeInsets& rhs) const {
return !(*this == rhs);
}
UIEdgeInsets UIEdgeInsets::operator+(const UIEdgeInsets& rhs) const {
return {top + rhs.top, left + rhs.left, bottom + rhs.bottom, right + rhs.right};
}
UIEdgeInsets UIEdgeInsets::operator-(const UIEdgeInsets& rhs) const {
return {top - rhs.top, left - rhs.left, bottom - rhs.bottom, right - rhs.right};
}
UIEdgeInsets& UIEdgeInsets::operator+=(const UIEdgeInsets& rhs) {
this->top += rhs.top;
this->left += rhs.left;
this->bottom += rhs.bottom;
this->right += rhs.right;
return *this;
}
UIEdgeInsets& UIEdgeInsets::operator-=(const UIEdgeInsets& rhs) {
this->top -= rhs.top;
this->left -= rhs.left;
this->bottom -= rhs.bottom;
this->right -= rhs.right;
return *this;
}
UIEdgeInsets UIEdgeInsets::zero = UIEdgeInsets();
}
+2
View File
@@ -25,6 +25,8 @@ void UIImageView::setImage(std::shared_ptr<UIImage> image) {
void UIImageView::updateTextureFromImage() {
if (_image) {
// _image->cgImage()
layer()->setContentsTemplateMode(_image->_isTemplate);
layer()->setContents(_image->cgImage());
layer()->setContentsScale(_image->scale());
+434
View File
@@ -0,0 +1,434 @@
//
// Created by Даниил Виноградов on 24.01.2025.
//
//
// UIScrollView.cpp
// SDLTest
//
// Created by Даниил Виноградов on 12.03.2023.
//
#include <UIScrollView.h>
#include <CATransaction.h>
#include <UIScrollViewExtensions/SpringTimingParameters.h>
#include <UIScrollViewExtensions/RubberBand.h>
namespace NXKit {
std::shared_ptr<UIScrollView> UIScrollView::init() {
return new_shared<UIScrollView>();
}
UIScrollView::UIScrollView(NXRect frame): UIView(frame) {
_panGestureRecognizer = new_shared<UIPanGestureRecognizer>();
_panGestureRecognizer->onStateChanged = [this](auto){ onPanGestureStateChanged(); };
addGestureRecognizer(_panGestureRecognizer);
setClipsToBounds(true);
// applyScrollIndicatorsStyle();
// [horizontalScrollIndicator, verticalScrollIndicator].forEach {
// $0.alpha = 0
// addSubview($0)
// }
}
//void UIScrollView::addSubview(std::shared_ptr<UIView> view) {
// UIView::addSubview(view);
// view->yoga->setPositionType(YGPositionTypeAbsolute);
//}
void UIScrollView::setContentOffset(NXPoint offset, bool animated) {
if (offset == contentOffset()) return;
// Cancel deceleration animations only when contentOffset gets set without animations.
// Otherwise we might cancel any "bounds" animations which are not iniated from velocity scrolling.
if (_isDecelerating && UIView::currentAnimationPrototype == nullptr) {
cancelDecelerationAnimations();
}
setBounds(NXRect(offset, bounds().size));
layoutScrollIndicatorsIfNeeded();
// otherwise everything subscribing to scrollViewDidScroll is implicitly animated from velocity scroll
CATransaction::begin();
CATransaction::setDisableActions(!animated);
if (!delegate.expired()) delegate.lock()->scrollViewDidScroll(shared_from_base<UIScrollView>());
CATransaction::commit();
}
void UIScrollView::setContentInsetAdjustmentBehavior(UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior) {
if (_contentInsetAdjustmentBehavior == contentInsetAdjustmentBehavior) return;
_contentInsetAdjustmentBehavior = contentInsetAdjustmentBehavior;
setNeedsLayout();
}
void UIScrollView::setBounceHorizontally(bool bounceHorizontally) {
if (_bounceHorizontally == bounceHorizontally) return;
_bounceHorizontally = bounceHorizontally;
setNeedsLayout();
}
void UIScrollView::setBounceVertically(bool bounceVertically) {
if (_bounceVertically == bounceVertically) return;
_bounceVertically = bounceVertically;
setNeedsLayout();
}
void UIScrollView::layoutMarginsDidChange() {
auto delta = _lastLayoutMargins - layoutMargins();
_lastLayoutMargins = layoutMargins();
auto target = getBoundsCheckedContentOffset(contentOffset() + NXPoint(delta.left, delta.top));
setContentOffset(target, false);
}
NXPoint UIScrollView::visibleContentOffset() {
return (layer()->presentationOrSelf())->bounds().origin;
}
NXSize UIScrollView::contentSize() {
if (subviews().size() == 0) return NXSize();
return subviews().front()->bounds().size;
}
NXPoint UIScrollView::getBoundsCheckedContentOffset(NXPoint newContentOffset) {
auto contentNXSize = this->contentSize();
auto contentHeight = contentNXSize.height;// fmaxf(contentNXSize.height, bounds().height());
auto contentWidth = contentNXSize.width;// fmaxf(contentNXSize.width, bounds().width());
auto allInsects = _contentInset;// + layoutMargins();
bool contentWidthGreaterThenScrollBounds = contentWidth > bounds().width() -_contentInset.left - _contentInset.right;
bool contentHeightGreaterThenScrollBounds = contentHeight > bounds().height() - _contentInset.top - _contentInset.bottom;
switch (_contentInsetAdjustmentBehavior) {
case UIScrollViewContentInsetAdjustmentBehavior::scrollableAxes: {
if (contentWidthGreaterThenScrollBounds || _bounceHorizontally) {
allInsects += UIEdgeInsets(0, layoutMargins().left, 0, layoutMargins().right);
}
if (contentHeightGreaterThenScrollBounds || _bounceVertically) {
allInsects += UIEdgeInsets(layoutMargins().top, 0, layoutMargins().bottom, 0);
}
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::always: {
allInsects += layoutMargins();
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::never: {
break;
}
}
bool contentWidthGreaterThenScrollSafeArea = contentWidth > bounds().width() -allInsects.left - allInsects.right;
bool contentHeightGreaterThenScrollSafeArea = contentHeight > bounds().height() - allInsects.top - allInsects.bottom;
NXPoint target;
if (!contentWidthGreaterThenScrollSafeArea) {
target.x = - allInsects.left;
} else {
target.x = fminf(fmaxf(newContentOffset.x, -allInsects.left), (contentWidth + allInsects.right) - bounds().width());
}
if (!contentHeightGreaterThenScrollSafeArea) {
target.y = - allInsects.top;
} else {
target.y = fminf(fmaxf(newContentOffset.y, -allInsects.top), (contentHeight + allInsects.bottom) - bounds().height());
}
return target;
}
NXRect UIScrollView::contentOffsetBounds() {
auto contentNXSize = this->contentSize();
auto contentHeight = contentNXSize.height;// fmaxf(contentNXSize.height, bounds().height());
auto contentWidth = contentNXSize.width;// fmaxf(contentNXSize.width, bounds().width());
auto allInsects = _contentInset;// + layoutMargins();
bool contentWidthGreaterThenScrollBounds = contentWidth > bounds().width() -_contentInset.left - _contentInset.right;
bool contentHeightGreaterThenScrollBounds = contentHeight > bounds().height() - _contentInset.top - _contentInset.bottom;
switch (_contentInsetAdjustmentBehavior) {
case UIScrollViewContentInsetAdjustmentBehavior::scrollableAxes: {
if (contentWidthGreaterThenScrollBounds || _bounceHorizontally) {
allInsects += UIEdgeInsets(0, layoutMargins().left, 0, layoutMargins().right);
}
if (contentHeightGreaterThenScrollBounds || _bounceVertically) {
allInsects += UIEdgeInsets(layoutMargins().top, 0, layoutMargins().bottom, 0);
}
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::always: {
allInsects += layoutMargins();
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::never: {
break;
}
}
bool contentWidthGreaterThenScrollSafeArea = contentWidth > bounds().width() -allInsects.left - allInsects.right;
bool contentHeightGreaterThenScrollSafeArea = contentHeight > bounds().height() - allInsects.top - allInsects.bottom;
NXRect resBounds;
if (!contentWidthGreaterThenScrollSafeArea) {
resBounds.origin.x = - allInsects.left;
resBounds.size.width = 0;
} else {
resBounds.origin.x = - allInsects.left;
resBounds.size.width = (contentWidth + allInsects.left + allInsects.right) - bounds().width();
}
if (!contentHeightGreaterThenScrollSafeArea) {
resBounds.origin.y = - allInsects.top;
resBounds.size.height = 0;
} else {
resBounds.origin.y = - allInsects.top;
resBounds.size.height = (contentHeight + allInsects.top + allInsects.bottom) - bounds().height();
}
return resBounds;
}
bool UIScrollView::shouldBounceHorizontally() {
auto contentNXSize = this->contentSize();
bool contentWidthGreaterThenScrollBounds = contentNXSize.width > bounds().width() -_contentInset.left - _contentInset.right;
auto allInsects = _contentInset;// + layoutMargins();
switch (_contentInsetAdjustmentBehavior) {
case UIScrollViewContentInsetAdjustmentBehavior::scrollableAxes: {
if (contentWidthGreaterThenScrollBounds || _bounceHorizontally) {
allInsects += UIEdgeInsets(0, layoutMargins().left, 0, layoutMargins().right);
}
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::always: {
allInsects += layoutMargins();
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::never: {
break;
}
}
bool contentWidthGreaterThenScrollSafeArea = contentNXSize.width > bounds().width() -allInsects.left - allInsects.right;
return contentWidthGreaterThenScrollSafeArea && _bounceHorizontally;
}
bool UIScrollView::shouldBounceVertically() {
auto contentNXSize = this->contentSize();
bool contentHeightGreaterThenScrollBounds = contentNXSize.height > bounds().height() - _contentInset.top - _contentInset.bottom;
auto allInsects = _contentInset;// + layoutMargins();
switch (_contentInsetAdjustmentBehavior) {
case UIScrollViewContentInsetAdjustmentBehavior::scrollableAxes: {
if (contentHeightGreaterThenScrollBounds || _bounceVertically) {
allInsects += UIEdgeInsets(layoutMargins().top, 0, layoutMargins().bottom, 0);
}
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::always: {
allInsects += layoutMargins();
break;
}
case UIScrollViewContentInsetAdjustmentBehavior::never: {
break;
}
}
bool contentHeightGreaterThenScrollSafeArea = contentNXSize.height > bounds().height() - allInsects.top - allInsects.bottom;
return contentHeightGreaterThenScrollSafeArea && _bounceVertically;
}
void UIScrollView::onPan() {
auto translation = _panGestureRecognizer->translationInView(shared_from_this());
// _panGestureRecognizer->setTranslation(NXPoint(), shared_from_this());
auto panGestureVelocity = _panGestureRecognizer->velocityIn(shared_from_this());
weightedAverageVelocity = weightedAverageVelocity * 0.2 + panGestureVelocity * 0.8;
auto offset = contentOffsetBounds();
auto rubberBand = RubberBand(0.55f, frame().size, contentOffsetBounds());
NXPoint clamped = getBoundsCheckedContentOffset(_initialContentOffset - translation);
NXPoint target = rubberBand.clamp(_initialContentOffset - translation);
if (!shouldBounceHorizontally()) {
target.x = clamped.x;
}
if (!shouldBounceVertically()) {
target.y = clamped.y;
}
setContentOffset(target, false);
// auto newOffset = getBoundsCheckedContentOffset(_initialContentOffset - translation);
// setContentOffset(newOffset, false);
}
void UIScrollView::onPanGestureStateChanged() {
switch (_panGestureRecognizer->state()) {
case UIGestureRecognizerState::possible: {
// showScrollIndicators();
cancelDeceleratingIfNeccessary();
break;
}
case UIGestureRecognizerState::began: {
printf("Began\n");
_initialContentOffset = contentOffset();
break;
}
case UIGestureRecognizerState::changed: {
if (!delegate.expired()) delegate.lock()->scrollViewWillBeginDragging(shared_from_base<UIScrollView>());
onPan();
break;
}
case UIGestureRecognizerState::ended: {
startDeceleratingIfNecessary();
weightedAverageVelocity = NXPoint();
break;
}
// XXX: Spring back with animation:
//case .ended, .cancelled:
//if contentOffset.x < _contentInset.left {
// setContentOffset(CGNXPoint(x: _contentInset.left, y: contentOffset.y), animated: true)
//}
default: break;
}
}
void UIScrollView::layoutScrollIndicatorsIfNeeded() {
}
void UIScrollView::showScrollIndicators() {
}
void UIScrollView::hideScrollIndicators() {
}
void UIScrollView::startDeceleratingIfNecessary() {
// Only animate if instantaneous velocity is large enough
// Otherwise we could animate after scrolling quickly, pausing for a few seconds, then letting go
// TODO: Need to check velocity, it's too weak
auto velocity = NXPoint(-weightedAverageVelocity.x * 10, -weightedAverageVelocity.y * 10);
if (!shouldBounceVertically()) {
velocity.y = 0;
}
if (!shouldBounceHorizontally()) {
velocity.x = 0;
}
auto decelerationRate = _decelerationRate.rawValue();
auto threshold = 0.5f / layer()->contentsScale();
auto parameters = DecelerationTimingParameters(contentOffset(),velocity, decelerationRate, threshold);
auto destination = parameters.destination();
auto clippedDestination = getBoundsCheckedContentOffset(destination);
bool isClipped = destination != clippedDestination;
float duration;
if (isClipped) {
duration = parameters.durationTo(clippedDestination);
} else {
duration = parameters.duration();
}
_isDecelerating = true;
_timerAnimation = std::make_shared<TimerAnimation>(duration, [this, parameters](float, double time) {
setContentOffset(parameters.valueAt(time), false);
}, [this, parameters, duration, isClipped](bool) {
_isDecelerating = isClipped;
if (isClipped) {
auto velocity = parameters.velocityAt(duration);
bounceWithVelocity(velocity);
}
});
// auto nonBoundsCheckedScrollAnimationDistance = weightedAverageVelocity * dampingFactor; // hand-tuned
// auto targetOffset = contentOffset() - nonBoundsCheckedScrollAnimationDistance;// getBoundsCheckedContentOffset(contentOffset() - nonBoundsCheckedScrollAnimationDistance);
// auto distanceToBoundsCheckedTarget = contentOffset() - targetOffset;
// auto velocityIsLargeEnoughToDecelerate = (velocity.magnitude() > 10);
// auto willDecelerate = (velocityIsLargeEnoughToDecelerate && distanceToBoundsCheckedTarget.magnitude() > 0.0);
//
// if (!delegate.expired()) delegate.lock()->scrollViewDidEndDragging(shared_from_base<UIScrollView>(), willDecelerate);
// if (!willDecelerate) { hideScrollIndicators(); return; }
//
// // https://ariya.io/2011/10/flick-list-with-its-momentum-scrolling-and-deceleration
// // TODO: This value should be calculated from `self.decelerationRate` instead
// // But actually we want to redo this function to avoid `UIView.animate` entirely,
// // in which case we wouldn't need an animationTime at all.
// auto animationTimeConstant = 0.325f * dampingFactor;
//
// // This calculation is a weird approximation but it's close enough for now...
// auto animationTime = logf(distanceToBoundsCheckedTarget.magnitude()) * animationTimeConstant;
//
// UIViewAnimationOptions options = UIViewAnimationOptions(UIViewAnimationOptions::beginFromCurrentState | UIViewAnimationOptions::customEaseOut | UIViewAnimationOptions::allowUserInteraction);
// UIView::animate(
// animationTime,
// 0,
// options,
// [this, targetOffset]() {
// _isDecelerating = true;
// setContentOffset(targetOffset, false);
// },
// [this](bool) {
// _isDecelerating = false;
// }
// );
}
void UIScrollView::bounceWithVelocity(NXPoint velocity) {
auto restOffset = getBoundsCheckedContentOffset(contentOffset());
auto displacement = contentOffset() - restOffset;
auto threshold = 0.5f / layer()->contentsScale();
auto spring = Spring(1, 100, 1);
auto parameters = SpringTimingParameters(spring, displacement,velocity, threshold);
auto duration = parameters.duration();
_timerAnimation = std::make_shared<TimerAnimation>(duration, [this, duration, restOffset, parameters](auto, float time){
setContentOffset(restOffset + parameters.valueAt(time), false);
}, [this](bool) {
_isDecelerating = false;
});
}
void UIScrollView::cancelDeceleratingIfNeccessary() {
if (!_isDecelerating) { return; }
// Get the presentation value from the current animation
if (_timerAnimation) _timerAnimation->invalidate();
setContentOffset(visibleContentOffset(), false);
cancelDecelerationAnimations();
_isDecelerating = false;
}
void UIScrollView::cancelDecelerationAnimations() {
// if (!layer()->animations.isEmpty) {
// layer.removeAnimation(forKey: "bounds")
// horizontalScrollIndicator.layer.removeAnimation(forKey: "position")
// verticalScrollIndicator.layer.removeAnimation(forKey: "position")
// }
layer()->removeAnimation("bounds");
}
//bool UIScrollView::applyXMLAttribute(std::string name, std::string value) {
// if (UIView::applyXMLAttribute(name, value)) { return true; }
//
// REGISTER_XIB_ATTRIBUTE(bounceVertically, valueToBool, setBounceVertically)
// REGISTER_XIB_ATTRIBUTE(bounceHorizontally, valueToBool, setBounceHorizontally)
//
// return false;
//}
void UIScrollView::layoutSubviews() {
UIView::layoutSubviews();
setContentOffset(getBoundsCheckedContentOffset(contentOffset()), false);
}
}
@@ -0,0 +1,41 @@
//
// Created by Daniil Vinogradov on 12/01/2024.
//
#include <UIScrollViewExtensions/DecelerationTimingParameters.h>
#include <cmath>
namespace NXKit {
NXPoint DecelerationTimingParameters::destination() const {
auto dCoeff = 1000 * log(decelerationRate);
auto res = initialValue - initialVelocity / dCoeff;
return res;
}
float DecelerationTimingParameters::duration() const {
if (initialVelocity.magnitude() <= 0) { return 0; }
auto dCoeff = 1000 * log(decelerationRate);
auto res = log(-dCoeff * threshold / initialVelocity.magnitude()) / dCoeff;
return res;
}
NXPoint DecelerationTimingParameters::valueAt(NXFloat time) const {
auto dCoeff = 1000 * log(decelerationRate);
auto res = initialValue + initialVelocity * (pow(decelerationRate, float(1000 * time)) - 1) / dCoeff;
return res;
}
float DecelerationTimingParameters::durationTo(NXPoint value) const {
if (value.distanceToSegment(initialValue, destination()) >= threshold) { return 0; }
auto dCoeff = 1000 * log(decelerationRate);
auto res = log(1.0f + dCoeff * (value - initialValue).magnitude() / initialVelocity.magnitude()) / dCoeff;
return res;
}
NXPoint DecelerationTimingParameters::velocityAt(NXFloat time) const {
return initialVelocity * pow(decelerationRate, NXFloat(1000 * time));
}
}
@@ -0,0 +1,15 @@
//
// Created by Даниил Виноградов on 12.01.2024.
//
#include <UIScrollViewExtensions/RubberBand.h>
namespace NXKit {
NXPoint RubberBand::clamp(NXPoint point) const{
auto x = Geometry::rubberBandClamp(point.x, coeff, dims.width, bounds.minX(), bounds.maxX());
auto y = Geometry::rubberBandClamp(point.y, coeff, dims.height, bounds.minY(), bounds.maxY());
return {x, y};
}
}
@@ -0,0 +1,112 @@
//
// Created by Даниил Виноградов on 12.01.2024.
//
#include <UIScrollViewExtensions/SpringTimingParameters.h>
namespace NXKit {
struct UnderdampedSpringTimingParameters: public TimingParameters {
Spring spring;
NXPoint displacement;
NXPoint initialVelocity;
NXFloat threshold;
UnderdampedSpringTimingParameters(Spring spring, NXPoint displacement, NXPoint initialVelocity, NXFloat threshold):
spring(spring),
displacement(displacement),
initialVelocity(initialVelocity),
threshold(threshold) {}
[[nodiscard]] NXPoint c2() const {
return (initialVelocity + displacement * spring.beta() ) / spring.dampedNaturalFrequency();
}
[[nodiscard]] NXFloat duration() const override {
if (displacement.magnitude() == 0 && initialVelocity.magnitude() == 0) {
return 0;
}
return NXFloat(log((displacement.magnitude() + c2().magnitude()) / threshold) / spring.beta());
}
NXPoint valueAt(NXFloat t) const override {
auto wd = spring.dampedNaturalFrequency();
return (displacement * cos(wd * t) + c2() * sin(wd * t)) * exp(-spring.beta() * t);
}
};
struct CriticallyDampedSpringTimingParameters: public TimingParameters {
Spring spring;
NXPoint displacement;
NXPoint initialVelocity;
NXFloat threshold;
CriticallyDampedSpringTimingParameters(Spring spring, NXPoint displacement, NXPoint initialVelocity, NXFloat threshold):
spring(spring),
displacement(displacement),
initialVelocity(initialVelocity),
threshold(threshold) {}
[[nodiscard]] NXPoint c2() const {
return initialVelocity + displacement * spring.beta();
}
[[nodiscard]] NXFloat duration() const override {
if (displacement.magnitude() == 0 && initialVelocity.magnitude() == 0) {
return 0;
}
auto b = spring.beta();
auto e = NXFloat(M_E);
auto t1 = 1 / b * log(2 * displacement.magnitude() / threshold);
auto t2 = 2 / b * log(4 * c2().magnitude() / (e * b * threshold));
return fmaxf(t1, t2);
}
NXPoint valueAt(NXFloat t) const override {
return (displacement + c2() * t) * exp(-spring.beta() * t);
}
};
NXFloat Spring::damping() const {
return 2 * dampingRatio * sqrt(mass * stiffness);
}
NXFloat Spring::beta() const {
return damping() / (2 * mass);
}
NXFloat Spring::dampedNaturalFrequency() const {
return sqrt(stiffness / mass) * sqrt(1 - dampingRatio * dampingRatio);
}
SpringTimingParameters::SpringTimingParameters(Spring spring, NXPoint displacement, NXPoint initialVelocity, NXFloat threshold):
spring(spring), displacement(displacement), initialVelocity(initialVelocity), threshold(threshold)
{
if (spring.dampingRatio == 1) {
impl = std::make_shared<CriticallyDampedSpringTimingParameters>(spring,
displacement,
initialVelocity,
threshold);
} else if (spring.dampingRatio > 0 && spring.dampingRatio < 1) {
impl = std::make_shared<UnderdampedSpringTimingParameters>(spring,
displacement,
initialVelocity,
threshold);
} else {
printf("dampingRatio should be greater than 0 and less than or equal to 1");
}
}
NXFloat SpringTimingParameters::duration() const {
auto res = impl->duration();
return res;
}
NXPoint SpringTimingParameters::valueAt(NXFloat time) const {
auto res = impl->valueAt(time);
return res;
}
}
@@ -0,0 +1,41 @@
//
// Created by Daniil Vinogradov on 12/01/2024.
//
#include <UIScrollViewExtensions/TimerAnimation.h>
#include <utility>
namespace NXKit {
TimerAnimation::TimerAnimation(double duration, std::function<void(float, double)> animations, std::function<void(bool)> completion):
displayLink(CADisplayLink([this]() {
handleFrame();
})),
duration(duration),
animations(std::move(animations)),
completion(std::move(completion))
{}
TimerAnimation::~TimerAnimation() {
invalidate();
}
void TimerAnimation::invalidate() {
if (!_running) return;
_running = false;
displayLink.invalidate();
}
void TimerAnimation::handleFrame() {
if (!_running) return;
auto elapsed = (Timer() - firstFrameTimestamp) / 1000;
if (elapsed >= duration) {
animations(1, duration);
_running = false;
completion(true);
displayLink.invalidate();
} else {
animations(elapsed / duration, elapsed);
}
}
}
+119 -2
View File
@@ -42,6 +42,11 @@ void UIView::setBounds(NXRect bounds) {
_layer->setBounds(bounds);
}
void UIView::setHidden(bool hidden) {
_layer->setHidden(hidden);
_yoga->setIncludedInLayout(!hidden);
}
void UIView::setCenter(NXPoint position) {
auto frame = this->frame();
frame.setMidX(position.x);
@@ -54,12 +59,124 @@ NXPoint UIView::center() const {
return { frame.midX(), frame.midY() };
}
void UIView::setInsetsLayoutMarginsFromSafeArea(bool insetsLayoutMarginsFromSafeArea) {
if (_insetsLayoutMarginsFromSafeArea == insetsLayoutMarginsFromSafeArea) return;
_insetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea;
setNeedsLayout();
}
void UIView::setPreservesSuperviewLayoutMargins(bool preservesSuperviewLayoutMargins) {
if (_preservesSuperviewLayoutMargins == preservesSuperviewLayoutMargins) return;
_preservesSuperviewLayoutMargins = preservesSuperviewLayoutMargins;
setNeedsLayout();
}
UIEdgeInsets UIView::layoutMargins() {
return _calculatedLayoutMargins;
}
void UIView::setLayoutMargins(UIEdgeInsets layoutMargins) {
if (_layoutMargins == layoutMargins) return;
_layoutMargins = layoutMargins;
setNeedsUpdateLayoutMargins();
setNeedsLayout();
}
void UIView::setSafeAreaInsets(UIEdgeInsets safeAreaInsets) {
if (_safeAreaInsets == safeAreaInsets) return;
_safeAreaInsets = safeAreaInsets;
setNeedsUpdateLayoutMargins();
updateSafeAreaInsetsInChilds();
safeAreaInsetsDidChange();
setNeedsLayout();
}
void UIView::updateSafeAreaInsetsInChilds() {
for (auto& subview: _subviews) {
subview->setNeedsUpdateSafeAreaInsets();
}
}
void UIView::updateSafeAreaInsetsIfNeeded() {
if (_needsUpdateSafeAreaInsets) {
_needsUpdateSafeAreaInsets = false;
updateSafeAreaInsets();
}
}
void UIView::updateSafeAreaInsets() {
if (_superview.expired()) {
if (shared_from_base<UIWindow>() != nullptr) return;
else return setSafeAreaInsets(UIEdgeInsets::zero);
}
auto parentSafeArea = _superview.lock()->_safeAreaInsets;
auto parentSize = _superview.lock()->bounds().size;
layoutIfNeeded();
auto frame = this->frame();
auto newSafeArea = UIEdgeInsets(fmaxf(0, parentSafeArea.top - fmaxf(0, frame.minY())),
fmaxf(0, parentSafeArea.left - fmaxf(0, frame.minX())),
fmaxf(0, parentSafeArea.bottom - fmaxf(0, (parentSize.height - frame.maxY()))),
fmaxf(0, parentSafeArea.right - fmaxf(0, (parentSize.width - frame.maxX()))));
if (!_parentController.expired()) {
newSafeArea += _parentController.lock()->additionalSafeAreaInsets();
}
setSafeAreaInsets(newSafeArea);
}
void UIView::updateLayoutMarginIfNeeded() {
if (_needsUpdateLayoutMargins) {
_needsUpdateLayoutMargins = false;
updateLayoutMargin();
}
}
void UIView::updateLayoutMargin() {
auto margins = _layoutMargins;
bool needsSuperviewMargins = _preservesSuperviewLayoutMargins && !superview().expired();
if (needsSuperviewMargins && _insetsLayoutMarginsFromSafeArea) {
auto superviewMargins = superview().lock()->layoutMargins();
auto maxCombination = UIEdgeInsets(fmaxf(_safeAreaInsets.top, superviewMargins.top),
fmaxf(_safeAreaInsets.left, superviewMargins.left),
fmaxf(_safeAreaInsets.bottom, superviewMargins.bottom),
fmaxf(_safeAreaInsets.right, superviewMargins.right));
margins += maxCombination;
} else {
if (_insetsLayoutMarginsFromSafeArea) {
margins += _safeAreaInsets;
}
if (needsSuperviewMargins) {
margins += superview().lock()->layoutMargins();
}
}
if (!_parentController.expired() && _parentController.lock()->viewRespectsSystemMinimumLayoutMargins()) {
auto minMargins = _parentController.lock()->systemMinimumLayoutMargins();
margins = UIEdgeInsets(fmaxf(margins.top, minMargins.top),
fmaxf(margins.left, minMargins.left),
fmaxf(margins.bottom, minMargins.bottom),
fmaxf(margins.right, minMargins.right));
}
if (_calculatedLayoutMargins != margins) {
_calculatedLayoutMargins = margins;
layoutMarginsDidChange();
}
}
void UIView::addGestureRecognizer(const std::shared_ptr<UIGestureRecognizer>& gestureRecognizer) {
gestureRecognizer->_view = weak_from_this();
_gestureRecognizers.push_back(gestureRecognizer);
}
void UIView::addSubview(std::shared_ptr<UIView> view) {
void UIView::addSubview(const std::shared_ptr<UIView>& view) {
bool needToNotifyViewController = false;
if (!view->_parentController.expired()) {
auto window = this->window();
@@ -92,7 +209,7 @@ void UIView::setSuperview(const std::shared_ptr<UIView>& superview) {
tintColorDidChange();
}
void UIView::insertSubviewAt(std::shared_ptr<UIView> view, int index) {
void UIView::insertSubviewAt(const std::shared_ptr<UIView>& view, int index) {
// TODO: Need to implement
}
@@ -67,11 +67,12 @@ void YogaTestViewController::loadView() {
auto buttonsBox = new_shared<UIView>();
auto res = romfs::get("img/star.png");
auto res = romfs::get("img/icons8-star-90.png");
auto data = new_shared<NXData>(res.data(), res.size());
auto image = UIImage::fromData(data, 3);
image->setRenderModeAsTemplate(true);
auto control = new_shared<UIButton>(UIButtonStyle::tinted);
auto control = new_shared<UIButton>(UIButtonStyle::filled);
control->layer()->setCornerRadius(10);
control->titleLabel()->setText("Buttogn!");
control->setImage(image);
@@ -82,6 +83,7 @@ void YogaTestViewController::loadView() {
auto control2 = new_shared<UIButton>(UIButtonStyle::tinted);
control2->layer()->setCornerRadius(10);
control2->titleLabel()->setText("Button!");
control2->setTintColor(UIColor::systemRed);
buttonsBox->addSubview(control2);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB