From c011de5ceb2a2e65882152dc71bebb51bbbf17ec Mon Sep 17 00:00:00 2001 From: Daniil Vinogradov Date: Fri, 24 Jan 2025 22:02:46 +0100 Subject: [PATCH] WIP: ScrollView --- Submodules/UIKit/CMakeLists.txt | 6 + Submodules/UIKit/include/CALayer.h | 4 + Submodules/UIKit/include/CGImage.h | 7 +- Submodules/UIKit/include/Geometry.h | 43 +- Submodules/UIKit/include/UIEdgeInsets.h | 33 ++ Submodules/UIKit/include/UIImage.h | 7 +- Submodules/UIKit/include/UIKit.h | 5 +- Submodules/UIKit/include/UIScrollView.h | 106 +++++ .../DecelerationTimingParameters.h | 32 ++ .../UIScrollViewExtensions/RubberBand.h | 22 + .../SpringTimingParameters.h | 44 ++ .../UIScrollViewExtensions/TimerAnimation.h | 31 ++ .../UIScrollViewDecelerationRate.h | 39 ++ Submodules/UIKit/include/UIView.h | 42 +- Submodules/UIKit/include/UIViewController.h | 10 +- Submodules/UIKit/lib/CALayer.cpp | 9 +- Submodules/UIKit/lib/Geometry.cpp | 50 +- Submodules/UIKit/lib/UIButton.cpp | 4 + Submodules/UIKit/lib/UIEdgeInsets.cpp | 53 +++ Submodules/UIKit/lib/UIImageView.cpp | 2 + Submodules/UIKit/lib/UIScrollView.cpp | 434 ++++++++++++++++++ .../DecelerationTimingParameters.cpp | 41 ++ .../lib/UIScrollViewExtensions/RubberBand.cpp | 15 + .../SpringTimingParameters.cpp | 112 +++++ .../UIScrollViewExtensions/TimerAnimation.cpp | 41 ++ Submodules/UIKit/lib/UIView.cpp | 121 ++++- .../YogaTestViewController.cpp | 6 +- resources/img/icons8-star-90.png | Bin 0 -> 1845 bytes 28 files changed, 1278 insertions(+), 41 deletions(-) create mode 100644 Submodules/UIKit/include/UIEdgeInsets.h create mode 100644 Submodules/UIKit/include/UIScrollView.h create mode 100644 Submodules/UIKit/include/UIScrollViewExtensions/DecelerationTimingParameters.h create mode 100644 Submodules/UIKit/include/UIScrollViewExtensions/RubberBand.h create mode 100644 Submodules/UIKit/include/UIScrollViewExtensions/SpringTimingParameters.h create mode 100644 Submodules/UIKit/include/UIScrollViewExtensions/TimerAnimation.h create mode 100644 Submodules/UIKit/include/UIScrollViewExtensions/UIScrollViewDecelerationRate.h create mode 100644 Submodules/UIKit/lib/UIEdgeInsets.cpp create mode 100644 Submodules/UIKit/lib/UIScrollView.cpp create mode 100644 Submodules/UIKit/lib/UIScrollViewExtensions/DecelerationTimingParameters.cpp create mode 100644 Submodules/UIKit/lib/UIScrollViewExtensions/RubberBand.cpp create mode 100644 Submodules/UIKit/lib/UIScrollViewExtensions/SpringTimingParameters.cpp create mode 100644 Submodules/UIKit/lib/UIScrollViewExtensions/TimerAnimation.cpp create mode 100644 resources/img/icons8-star-90.png diff --git a/Submodules/UIKit/CMakeLists.txt b/Submodules/UIKit/CMakeLists.txt index 6b24993..239e906 100644 --- a/Submodules/UIKit/CMakeLists.txt +++ b/Submodules/UIKit/CMakeLists.txt @@ -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 ) diff --git a/Submodules/UIKit/include/CALayer.h b/Submodules/UIKit/include/CALayer.h index 134e4ed..7097f81 100644 --- a/Submodules/UIKit/include/CALayer.h +++ b/Submodules/UIKit/include/CALayer.h @@ -75,6 +75,9 @@ public: void setMask(const std::shared_ptr& mask); [[nodiscard]] std::shared_ptr 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 _contents; bool _masksToBounds = false; diff --git a/Submodules/UIKit/include/CGImage.h b/Submodules/UIKit/include/CGImage.h index 4808a2b..78e67c2 100644 --- a/Submodules/UIKit/include/CGImage.h +++ b/Submodules/UIKit/include/CGImage.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -14,10 +15,10 @@ public: sk_sp pointee; // CGImage(NXSize size); - CGImage(const std::shared_ptr& sourceData); + explicit CGImage(const std::shared_ptr& sourceData); // CGImage(SDL_Surface* surface); - CGImage(sk_sp image, std::shared_ptr sourceData); - CGImage(sk_sp image): CGImage(image, nullptr) {} + explicit CGImage(sk_sp image, std::shared_ptr sourceData); + explicit CGImage(sk_sp image): CGImage(std::move(image), nullptr) {} ~CGImage(); [[nodiscard]] NXSize size() const; diff --git a/Submodules/UIKit/include/Geometry.h b/Submodules/UIKit/include/Geometry.h index a867344..f38d4f7 100644 --- a/Submodules/UIKit/include/Geometry.h +++ b/Submodules/UIKit/include/Geometry.h @@ -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); +}; + } diff --git a/Submodules/UIKit/include/UIEdgeInsets.h b/Submodules/UIKit/include/UIEdgeInsets.h new file mode 100644 index 0000000..1518fe1 --- /dev/null +++ b/Submodules/UIKit/include/UIEdgeInsets.h @@ -0,0 +1,33 @@ +// +// UIEdgeInsets.hpp +// SDLTest +// +// Created by Даниил Виноградов on 12.03.2023. +// + +#pragma once + +#include + +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; +}; + +} diff --git a/Submodules/UIKit/include/UIImage.h b/Submodules/UIKit/include/UIImage.h index 0f4c888..2ee2330 100644 --- a/Submodules/UIKit/include/UIImage.h +++ b/Submodules/UIKit/include/UIImage.h @@ -13,11 +13,16 @@ public: static std::shared_ptr fromPath(const std::string& path); static std::shared_ptr fromData(const std::shared_ptr& data, NXFloat scale = SkiaCtx::main()->getScaleFactor()); - std::shared_ptr cgImage() { return _cgImage; } + [[nodiscard]] std::shared_ptr 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; NXSize _size; NXFloat _scale; diff --git a/Submodules/UIKit/include/UIKit.h b/Submodules/UIKit/include/UIKit.h index 2fd3920..7faacaa 100644 --- a/Submodules/UIKit/include/UIKit.h +++ b/Submodules/UIKit/include/UIKit.h @@ -10,9 +10,10 @@ #include #include #include -#include -#include #include +#include +#include +#include #include #include diff --git a/Submodules/UIKit/include/UIScrollView.h b/Submodules/UIKit/include/UIScrollView.h new file mode 100644 index 0000000..607e117 --- /dev/null +++ b/Submodules/UIKit/include/UIScrollView.h @@ -0,0 +1,106 @@ +// +// UIScrollView.hpp +// SDLTest +// +// Created by Даниил Виноградов on 12.03.2023. +// + +#pragma once + +#include +#include +#include +#include +#include + +namespace NXKit { + +class UIScrollView; +class UIScrollViewDelegate { +public: + virtual void scrollViewWillBeginDragging(std::shared_ptr scrollView) {} + virtual void scrollViewDidScroll(std::shared_ptr scrollView) {} + virtual void scrollViewDidEndDragging(std::shared_ptr scrollView, bool willDecelerate) {} +}; + +enum class UIScrollViewContentInsetAdjustmentBehavior { + scrollableAxes, + never, + always +}; + +class UIScrollView: public UIView { +public: + static std::shared_ptr init(); + + std::weak_ptr delegate; + + UIScrollView(NXRect frame = NXRect()); + +// virtual void addSubview(std::shared_ptr 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 _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; + 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); +}; + +} diff --git a/Submodules/UIKit/include/UIScrollViewExtensions/DecelerationTimingParameters.h b/Submodules/UIKit/include/UIScrollViewExtensions/DecelerationTimingParameters.h new file mode 100644 index 0000000..bebf61a --- /dev/null +++ b/Submodules/UIKit/include/UIScrollViewExtensions/DecelerationTimingParameters.h @@ -0,0 +1,32 @@ +// +// Created by Даниил Виноградов on 24.01.2025. +// + +#pragma once + +#include +#include + +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; +}; +} \ No newline at end of file diff --git a/Submodules/UIKit/include/UIScrollViewExtensions/RubberBand.h b/Submodules/UIKit/include/UIScrollViewExtensions/RubberBand.h new file mode 100644 index 0000000..db92f42 --- /dev/null +++ b/Submodules/UIKit/include/UIScrollViewExtensions/RubberBand.h @@ -0,0 +1,22 @@ +// +// Created by Даниил Виноградов on 12.01.2024. +// + +#pragma once + +#include + +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; +}; + +} \ No newline at end of file diff --git a/Submodules/UIKit/include/UIScrollViewExtensions/SpringTimingParameters.h b/Submodules/UIKit/include/UIScrollViewExtensions/SpringTimingParameters.h new file mode 100644 index 0000000..841ec2c --- /dev/null +++ b/Submodules/UIKit/include/UIScrollViewExtensions/SpringTimingParameters.h @@ -0,0 +1,44 @@ +// +// Created by Даниил Виноградов on 12.01.2024. +// + +#pragma once + +#include +#include + +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 impl; +}; + +} diff --git a/Submodules/UIKit/include/UIScrollViewExtensions/TimerAnimation.h b/Submodules/UIKit/include/UIScrollViewExtensions/TimerAnimation.h new file mode 100644 index 0000000..8d421c7 --- /dev/null +++ b/Submodules/UIKit/include/UIScrollViewExtensions/TimerAnimation.h @@ -0,0 +1,31 @@ +// +// Created by Daniil Vinogradov on 12/01/2024. +// + +#pragma once + +#include +#include + +namespace NXKit { + +class TimerAnimation: enable_shared_from_this_pointer_holder { +public: + TimerAnimation(double duration, std::function animations, std::function completion = [](bool _){}); + ~TimerAnimation(); + + void invalidate(); + +private: + CADisplayLink displayLink; + bool _running = true; + Timer firstFrameTimestamp; + double duration; + std::function animations; + std::function completion; + + void handleFrame(); +}; + +} + diff --git a/Submodules/UIKit/include/UIScrollViewExtensions/UIScrollViewDecelerationRate.h b/Submodules/UIKit/include/UIScrollViewExtensions/UIScrollViewDecelerationRate.h new file mode 100644 index 0000000..d0d4193 --- /dev/null +++ b/Submodules/UIKit/include/UIScrollViewExtensions/UIScrollViewDecelerationRate.h @@ -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; +}; +} \ No newline at end of file diff --git a/Submodules/UIKit/include/UIView.h b/Submodules/UIKit/include/UIView.h index 985199b..1ed9499 100644 --- a/Submodules/UIKit/include/UIView.h +++ b/Submodules/UIKit/include/UIView.h @@ -2,6 +2,7 @@ #include "CALayer.h" #include +#include #include #include #include @@ -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 backbroundColor) { _layer->setBackgroundColor(backbroundColor); } + void setBackgroundColor(const std::optional& backbroundColor) { _layer->setBackgroundColor(backbroundColor); } [[nodiscard]] std::optional backgroundColor() const { return _layer->backgroundColor(); } void setTintColor(std::optional tintColor); @@ -62,8 +63,8 @@ public: void addGestureRecognizer(const std::shared_ptr& gestureRecognizer); [[nodiscard]] std::vector>* gestureRecognizers() { return &_gestureRecognizers; } - void addSubview(std::shared_ptr view); - void insertSubviewAt(std::shared_ptr view, int index); + void addSubview(const std::shared_ptr& view); + void insertSubviewAt(const std::shared_ptr& view, int index); void insertSubviewBelow(const std::shared_ptr& view, const std::shared_ptr& belowSubview); void removeFromSuperview(); @@ -77,6 +78,20 @@ public: void traitCollectionDidChange(std::shared_ptr 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 layer) override; + void display(std::shared_ptr layer) override; private: friend class UIViewController; friend class UIFocusSystem; @@ -155,8 +170,25 @@ private: std::optional _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); diff --git a/Submodules/UIKit/include/UIViewController.h b/Submodules/UIKit/include/UIViewController.h index 2da1d54..eeb7aa1 100644 --- a/Submodules/UIKit/include/UIViewController.h +++ b/Submodules/UIKit/include/UIViewController.h @@ -34,10 +34,10 @@ public: virtual void didMoveToParent(std::shared_ptr 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 _view; std::weak_ptr _parent; std::vector> _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; diff --git a/Submodules/UIKit/lib/CALayer.cpp b/Submodules/UIKit/lib/CALayer.cpp index 1c77cb8..c8f683b 100644 --- a/Submodules/UIKit/lib/CALayer.cpp +++ b/Submodules/UIKit/lib/CALayer.cpp @@ -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(); diff --git a/Submodules/UIKit/lib/Geometry.cpp b/Submodules/UIKit/lib/Geometry.cpp index 4e190d5..108bfaf 100644 --- a/Submodules/UIKit/lib/Geometry.cpp +++ b/Submodules/UIKit/lib/Geometry.cpp @@ -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); +} diff --git a/Submodules/UIKit/lib/UIButton.cpp b/Submodules/UIKit/lib/UIButton.cpp index b16d6cf..7345346 100644 --- a/Submodules/UIKit/lib/UIButton.cpp +++ b/Submodules/UIKit/lib/UIButton.cpp @@ -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; } } diff --git a/Submodules/UIKit/lib/UIEdgeInsets.cpp b/Submodules/UIKit/lib/UIEdgeInsets.cpp new file mode 100644 index 0000000..d4788e0 --- /dev/null +++ b/Submodules/UIKit/lib/UIEdgeInsets.cpp @@ -0,0 +1,53 @@ +// +// UIEdgeInsets.cpp +// SDLTest +// +// Created by Даниил Виноградов on 12.03.2023. +// + +#include + +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(); + +} diff --git a/Submodules/UIKit/lib/UIImageView.cpp b/Submodules/UIKit/lib/UIImageView.cpp index 8304f9f..0585b8c 100644 --- a/Submodules/UIKit/lib/UIImageView.cpp +++ b/Submodules/UIKit/lib/UIImageView.cpp @@ -25,6 +25,8 @@ void UIImageView::setImage(std::shared_ptr image) { void UIImageView::updateTextureFromImage() { if (_image) { +// _image->cgImage() + layer()->setContentsTemplateMode(_image->_isTemplate); layer()->setContents(_image->cgImage()); layer()->setContentsScale(_image->scale()); diff --git a/Submodules/UIKit/lib/UIScrollView.cpp b/Submodules/UIKit/lib/UIScrollView.cpp new file mode 100644 index 0000000..2ace7cd --- /dev/null +++ b/Submodules/UIKit/lib/UIScrollView.cpp @@ -0,0 +1,434 @@ +// +// Created by Даниил Виноградов on 24.01.2025. +// +// +// UIScrollView.cpp +// SDLTest +// +// Created by Даниил Виноградов on 12.03.2023. +// + +#include +#include +#include +#include + +namespace NXKit { + +std::shared_ptr UIScrollView::init() { + return new_shared(); +} + +UIScrollView::UIScrollView(NXRect frame): UIView(frame) { + _panGestureRecognizer = new_shared(); + _panGestureRecognizer->onStateChanged = [this](auto){ onPanGestureStateChanged(); }; + addGestureRecognizer(_panGestureRecognizer); + setClipsToBounds(true); + +// applyScrollIndicatorsStyle(); +// [horizontalScrollIndicator, verticalScrollIndicator].forEach { +// $0.alpha = 0 +// addSubview($0) +// } +} + +//void UIScrollView::addSubview(std::shared_ptr 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()); + 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()); + 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(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(), 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(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); +} + +} diff --git a/Submodules/UIKit/lib/UIScrollViewExtensions/DecelerationTimingParameters.cpp b/Submodules/UIKit/lib/UIScrollViewExtensions/DecelerationTimingParameters.cpp new file mode 100644 index 0000000..0e232e0 --- /dev/null +++ b/Submodules/UIKit/lib/UIScrollViewExtensions/DecelerationTimingParameters.cpp @@ -0,0 +1,41 @@ +// +// Created by Daniil Vinogradov on 12/01/2024. +// + +#include +#include + +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)); +} +} \ No newline at end of file diff --git a/Submodules/UIKit/lib/UIScrollViewExtensions/RubberBand.cpp b/Submodules/UIKit/lib/UIScrollViewExtensions/RubberBand.cpp new file mode 100644 index 0000000..4a8a176 --- /dev/null +++ b/Submodules/UIKit/lib/UIScrollViewExtensions/RubberBand.cpp @@ -0,0 +1,15 @@ +// +// Created by Даниил Виноградов on 12.01.2024. +// + +#include + +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}; +} + +} \ No newline at end of file diff --git a/Submodules/UIKit/lib/UIScrollViewExtensions/SpringTimingParameters.cpp b/Submodules/UIKit/lib/UIScrollViewExtensions/SpringTimingParameters.cpp new file mode 100644 index 0000000..b2291a6 --- /dev/null +++ b/Submodules/UIKit/lib/UIScrollViewExtensions/SpringTimingParameters.cpp @@ -0,0 +1,112 @@ +// +// Created by Даниил Виноградов on 12.01.2024. +// + +#include + +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(spring, + displacement, + initialVelocity, + threshold); + } else if (spring.dampingRatio > 0 && spring.dampingRatio < 1) { + impl = std::make_shared(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; +} +} diff --git a/Submodules/UIKit/lib/UIScrollViewExtensions/TimerAnimation.cpp b/Submodules/UIKit/lib/UIScrollViewExtensions/TimerAnimation.cpp new file mode 100644 index 0000000..b4cead0 --- /dev/null +++ b/Submodules/UIKit/lib/UIScrollViewExtensions/TimerAnimation.cpp @@ -0,0 +1,41 @@ +// +// Created by Daniil Vinogradov on 12/01/2024. +// + +#include +#include + +namespace NXKit { + +TimerAnimation::TimerAnimation(double duration, std::function animations, std::function 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); + } +} +} \ No newline at end of file diff --git a/Submodules/UIKit/lib/UIView.cpp b/Submodules/UIKit/lib/UIView.cpp index 75f3e14..f9defc0 100644 --- a/Submodules/UIKit/lib/UIView.cpp +++ b/Submodules/UIKit/lib/UIView.cpp @@ -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() != 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& gestureRecognizer) { gestureRecognizer->_view = weak_from_this(); _gestureRecognizers.push_back(gestureRecognizer); } -void UIView::addSubview(std::shared_ptr view) { +void UIView::addSubview(const std::shared_ptr& view) { bool needToNotifyViewController = false; if (!view->_parentController.expired()) { auto window = this->window(); @@ -92,7 +209,7 @@ void UIView::setSuperview(const std::shared_ptr& superview) { tintColorDidChange(); } -void UIView::insertSubviewAt(std::shared_ptr view, int index) { +void UIView::insertSubviewAt(const std::shared_ptr& view, int index) { // TODO: Need to implement } diff --git a/app/Screens/YogaTestViewController/YogaTestViewController.cpp b/app/Screens/YogaTestViewController/YogaTestViewController.cpp index b83acaa..a73094f 100644 --- a/app/Screens/YogaTestViewController/YogaTestViewController.cpp +++ b/app/Screens/YogaTestViewController/YogaTestViewController.cpp @@ -67,11 +67,12 @@ void YogaTestViewController::loadView() { auto buttonsBox = new_shared(); - auto res = romfs::get("img/star.png"); + auto res = romfs::get("img/icons8-star-90.png"); auto data = new_shared(res.data(), res.size()); auto image = UIImage::fromData(data, 3); + image->setRenderModeAsTemplate(true); - auto control = new_shared(UIButtonStyle::tinted); + auto control = new_shared(UIButtonStyle::filled); control->layer()->setCornerRadius(10); control->titleLabel()->setText("Buttogn!"); control->setImage(image); @@ -82,6 +83,7 @@ void YogaTestViewController::loadView() { auto control2 = new_shared(UIButtonStyle::tinted); control2->layer()->setCornerRadius(10); control2->titleLabel()->setText("Button!"); + control2->setTintColor(UIColor::systemRed); buttonsBox->addSubview(control2); diff --git a/resources/img/icons8-star-90.png b/resources/img/icons8-star-90.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd0dc66286dae3ff3b2d562118ea8f3c93b094f GIT binary patch literal 1845 zcmV-52g>+~P)MI6V!d)-af>?y82pto6?nXQ#&S!M}xLnT6 z`OUd=@0ok=eBcM-p83tp_ntFnX3lSZfXCzUcsw4DC!E1-o;>m@fPn24uXKJmCqrbi zE&ykHvF-C@me6FgywGMG2jF%9Pid}c3=w*h=K9$HZe!nNO$xv`03QH2$ih9Lxc0md zk<&IH;tA*kumZrfTxZ$P1Hc0SwugvzuIBDk2~C2AF(BmHZ2;~E&>baA3cy5#z+@XV zWjAYt-l8eDNl|7OfO`N`qQp2Kz#5HMWHS|IFNi2dHb_x+Rzx}aacfZm908yqV+_`r zYO2;CDEqXcY=1%7PA*SvElN%T@Qt9O0f6r{etr>@->NCUUf71!08T-XaRq=sgdO}A zzzBt(TA>fJAqpSl`#YhJ-vJCqk#IeLc0s4U0&q=)uV)N|o)h6~41li%eyHu&!EtWk zF;CZ#7JlPb2>fj|5PG}7C;fdAfF{AXO*14Xxe>sjhz=|R&{yd1Vgr%WHdN^QL;&wa z_@cht0LQopz`vSCcLA7dxH6j;L+ED>{L|h22!O*He{BFRg(I8};4e*M_w!O4h9c~Y zA@mO6kd(=I0IeFIdjOn@gsF^}!uVI98iwaUZu7fZuWudRL6$Bh6_c{TRW0tjoc#vx}V| zM=P2|N0Pj3LFoBOcfxg20CrmtnwJ0FlJ1Pl)L9TYZL0kf4}gUhgK6*y)~SR3n1;ysL@%&h~!4CI-C0Df?w zvq3`k6Nf+)1bSQ+JJ9K%z(!NLW03%pBEGDYinSMFsEC5%T(#vHqD=E$VLaof+wFyh=mR)ua3XUW$bp4Xf5reU+B3kh=4o(B*)z*-s|Tm?R8Q_@F=^Yiv+dLQy^@ zOmJi!jMKbSt)IsINKx5q20rQ?5IJqMn3A$c;e(z|>r7Kr*2;IP zgvAaBoH>V<(2G}&VLs@6V;$XW;Z8yMD#Z#jDQgi1mrqfYCoN7uo>-}EHS0bxzUdvDT|HCA1~}&``~J zammL4E6gxFq_fe2AnUTy`V6r2$xM}yh5GVq$n~_jR4^(-I{A{~+B0($qKjWzVd;6Q zIMq+Cn^b2A&(my7;=JJ{SskS5TNn@JAOH-~E z`K7@jk!-Sb^+6o5>t4%7!$-U%GyF5Dk#&P z7X}?1%CL~Xa9qjxM^EM-v6}xqi2pW~Q_-*Cvh#$3Q6{pVGulb1?Td~SFQ4m$ogQd8 zu6n&Np1Yd!^^Gm{{L&Sia4;W-j@mipT1g8ip-+L1KYz?gY0H)|0Nxj-J&tP!IsSs` zDtl@;Ze*z-Z>YqYc=vCbX0xd5NpY;-WX_tY