Skip to content

Commit

Permalink
[SUTK] ScrollContainer improvements
Browse files Browse the repository at this point in the history
 - Corrections in the scrolling logic/calculation
 - Added unit test for Scroll Container (test.ScrollContainer)
 - Added more methods to interact with ScrollContainer and Scrollable
  • Loading branch information
ravi688 committed Jan 10, 2025
1 parent e221eb1 commit 1f6128b
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 31 deletions.
2 changes: 1 addition & 1 deletion dependencies/Common
21 changes: 16 additions & 5 deletions sutk/include/sutk/ScrollContainer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@

namespace SUTK
{
class ScrollContainer : public SUTK::Container,
class Scrollable;
class SUTK_API ScrollContainer : public SUTK::Container,
public SUTK::MouseScrollHandlerObject,
public SUTK::KeyboardHandlerObject
{
friend class Scrollable;
private:
SUTK::ModifierKeys m_modifiers;
// In centimeters
f32 m_scaleFactor;
u32 m_numKeysPressed;
Vec2Df m_scrollDelta;
Scrollable* m_scrollable { };
Compass4D<com::Bool> getScrollableCompass() const noexcept;
void onScrollableAdd(Scrollable* scrollable) noexcept;
protected:
// Overrides of SUTK::MouseScrollHandlerObject
virtual bool onMouseScroll(SUTK::Vec2Df scrollDelta) override;
Expand All @@ -24,15 +29,21 @@ namespace SUTK
public:
ScrollContainer(UIDriver& driver, Container* parent = NULL) noexcept;

// direction: 1 means towards right, -1 means towards left
com::Bool isScrollableTowards(f32 direction) noexcept { return (direction > 0) ? isRightScrollable() : isLeftScrollable(); }
com::Bool isLeftScrollable() noexcept;
com::Bool isRightScrollable() noexcept;
// direction: +ve means towards right, -ve means towards left
com::Bool isScrollableHorziontal(f32 direction) const noexcept;
// direction: +ve means downwards, -ve means upwards
com::Bool isScrollableVertical(f32 direction) const noexcept;
com::Bool isScrollableLeft() const noexcept { return isScrollableHorziontal(-1); };
com::Bool isScrollableRight() const noexcept { return isScrollableHorziontal(1); }
com::Bool isScrollableUp() const noexcept { return isScrollableVertical(-1); }
com::Bool isScrollableDown() const noexcept { return isScrollableVertical(1); }

void setScaleFactor(f32 scaleFactor) noexcept { m_scaleFactor = scaleFactor; }
f32 getScaleFactor() const noexcept { return m_scaleFactor; }
// scrollDelta must be in centimeters
void addScrollDelta(Vec2Df scrollDelta) noexcept;
Vec2Df getScrollDelta() const noexcept { return m_scrollDelta; }
// The internal accrued scroll delta becomes zero, so all the previous scroll will be reset to zero.
void resetScrollDelta() noexcept;
};
}
29 changes: 29 additions & 0 deletions sutk/include/sutk/Scrollable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,41 @@ namespace SUTK
class Scrollable
{
private:
// pointer to the Container object which is driven by this Scrollable "attribute" class (actually it's the Scroll Container which drives)
Container* m_container;
ScrollContainer* m_scrollContainer;
RectMarginsf m_scrollMargins { };
RectEdgeBits m_fullMarginEdges { };
protected:
ScrollContainer* getScrollContainer() noexcept { return m_scrollContainer; }
const ScrollContainer* getScrollContainer() const noexcept { return m_scrollContainer; }
Scrollable(Container* container) noexcept;
public:
virtual ~Scrollable() noexcept = default;

// Setters

// margins.left: is the max distance of the left edge of this Scrollable's Rect to the left edge of the ScrollbleContainer's Rect
// If it is zero then the Scrollable can't be scrolled rightwards detaching its left edge off the left edge of the ScrollContainer's Rect
// margins.right: is the max distance of the right edge of this Scrollable's Rect to the right edge of the ScrollableContainer's Rect
// If it is zero then the Scrollable can't be scrolled leftwards detaching its right edge off the right edge of the ScrollContainer's Rect
// margins.top: is the max distance of the top edge of this Scrollable's Rect to the top edge of the ScrollContainer's Rect
// If it is zero then the Scrollable can't be scrolled downwards detaching its top edge off the top edge of the ScrollContainer's Rect
// margins.bottom: is the max distance of the bottom edge of this Scrollable's Rect to the bottom edge of the ScrollContainer's Rect
// If it is zero then the Scrollable can't be scrolled upwards detaching its bottom edge off the bottom edge of the ScrollContainer's Rect
void setScrollMargins(RectMarginsf margins) noexcept;
// edgeBits: Bit Mask telling which sides/edges of this Scrollable's Rect will have full margin
// (if left/right then its the ScrollContainer's width, if top/bottom then its the ScrollContainer's height).
// RectEdgeBits::Left means margins.left will be overriden with ScrollContainer's width
// RectEdgeBits::Right means margins.right will be overriden with ScrollContainer's width
// RectEdgeBits::Top means margins.top will be overriden with ScrollContainer's height
// RectEdgeBits::Bottom means margins.bottom will be overriden with ScrollContainer's height
void setFullScrollMargins(RectEdgeBits edgeBits) noexcept;

// Getters
// If any of the flags in m_fullMarginEdges are set then it overrides the corresponding margin in the return copy of m_scrollMargins.
RectMarginsf getScrollMargins() const noexcept;
Container* getContainer() noexcept { return m_container; }
const Container* getContainer() const noexcept { return m_container; }
};
}
82 changes: 80 additions & 2 deletions sutk/include/sutk/defines.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,60 @@ namespace SUTK
template<>
struct NegativeSign<f64> { static constexpr f64 value = -1.0; };

// Enumeration to represent cardination directions or sides of a rectangular object
enum class CardinalDir : u8
{
Left,
Right,
Up,
Down,
Top = Up,
Bottom = Down,
West = Left,
East = Right,
North = Up,
South = Down
};

enum class CardinalDirBits : u8
{
Left = BIT8(com::EnumClassToInt(CardinalDir::Left)),
Right = BIT8(com::EnumClassToInt(CardinalDir::Right)),
Up = BIT8(com::EnumClassToInt(CardinalDir::Up)),
Down = BIT8(com::EnumClassToInt(CardinalDir::Down)),
Top = BIT8(com::EnumClassToInt(CardinalDir::Top)),
Bottom = BIT8(com::EnumClassToInt(CardinalDir::Bottom)),
West = BIT8(com::EnumClassToInt(CardinalDir::West)),
East = BIT8(com::EnumClassToInt(CardinalDir::East)),
North = BIT8(com::EnumClassToInt(CardinalDir::North)),
South = BIT8(com::EnumClassToInt(CardinalDir::South))
};

// Enumeration to represent sides of a rectangular object
using RectEdge = CardinalDir;
using RectEdgeBits = CardinalDirBits;

template<typename T>
struct Compass4D
{
T left;
T right;
union
{
struct
{
T top;
T bottom;
};
struct
{
T up;
T down;
};
};
};

SUTK_API std::ostream& operator<<(std::ostream& stream, CardinalDir cd) noexcept;

template<typename T>
union Vec2D
Expand Down Expand Up @@ -295,17 +349,27 @@ namespace SUTK
template<typename T>
struct Rect2D
{
T x;
T y;
union
{
T x;
T left;
};
union
{
T y;
T right;
};
union
{
T width;
T z;
T top;
};
union
{
T height;
T w;
T bottom;
};

// getters
Expand Down Expand Up @@ -356,6 +420,16 @@ namespace SUTK
return x;
}

f32 getTop() const noexcept
{
return y;
}

f32 getBottom() const noexcept
{
return height + y;
}

Vec2D<T> getTopLeft() const noexcept
{
return getPosition();
Expand Down Expand Up @@ -494,6 +568,9 @@ namespace SUTK
}
};

template<typename T>
using RectMargins = Rect2D<T>;

template<typename T>
Rect2D<T> Rect2D<T>::zero() { return { 0, 0, 0, 0 }; }

Expand Down Expand Up @@ -648,6 +725,7 @@ namespace SUTK
}

typedef Rect2D<f32> Rect2Df;
typedef RectMargins<f32> RectMarginsf;
typedef Vec2D<f32> Vec2Df;
typedef Vec3D<f32> Vec3Df;
typedef Rect2D<f32> Vec4Df;
Expand Down
1 change: 1 addition & 0 deletions sutk/source/AutoTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ int main()
std::cout << std::endl;
i += 1;
}
std::cout << "Total number of tests ran: " << tests->size() << "\n";
if(failedTests.size() == 0)
std::cout << "All tests passed" << std::endl;
else
Expand Down
2 changes: 1 addition & 1 deletion sutk/source/NotebookView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ namespace SUTK
void ScrollableTabBar::update()
{
auto* scrollCont = getScrollContainer();
if(scrollCont->isScrollableTowards(m_autoScrollDir))
if(scrollCont->isScrollableHorziontal(m_autoScrollDir))
{
auto dt = getUIDriver().getDeltaTime();
auto delta = Vec2Df::right() * m_autoScrollDir * dt * 5.0f;
Expand Down
108 changes: 87 additions & 21 deletions sutk/source/ScrollContainer.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <sutk/ScrollContainer.hpp>
#include <sutk/Scrollable.hpp>
#include <common/assert.h> // for _com_assert
#include <common/debug.hpp> // for com::source_loc()

Expand Down Expand Up @@ -48,34 +49,99 @@ namespace SUTK
}
}

com::Bool ScrollContainer::isLeftScrollable() noexcept
void ScrollContainer::onScrollableAdd(Scrollable* scrollable) noexcept
{
return com::True;
com_assert(COM_DESCRIPTION(!m_scrollable), "Only one child (Scrollable) can be added into a Scrollable Container, you need to first remove the older child");
m_scrollable = scrollable;
}

com::Bool ScrollContainer::isRightScrollable() noexcept
Compass4D<com::Bool> ScrollContainer::getScrollableCompass() const noexcept
{
return com::Bool { m_scrollDelta.x < Vec2Df::zero().x };
// If there is nothing to scroll then all directions are non-scrollable
if(!m_scrollable)
return { };

auto size = getSize();
auto margins = m_scrollable->getScrollMargins();
auto rect = m_scrollable->getContainer()->getRect();

Compass4D<com::Bool> cp;

// Can only be scrolled downwards if the distance of the top edge of the Scrollable's Rect from the top edge of the Scroll Container's Rect is less than the top margin
auto topDistance = rect.getTop();
cp.down = com::Bool { topDistance < margins.top };

// Can only be scrolled upwards if the distance of the bottom edge of the Scrollable's Rect from the bottom edge of the Scroll Container's Rect is less than the bottom margin
auto bottomDistance = size.height - rect.getBottom();
cp.up = com::Bool { bottomDistance < margins.bottom };

// Can only be scrolled towards right if the distance of the left edge of the Scrollable's Rect from the left edge of the Scroll Container's Rect is less than the left margin
auto leftDistance = rect.getLeft();;
cp.right = com::Bool { leftDistance < margins.left };

// Can only be scrolled towards left if the distance of the right edge of the Scrollable's Rect from the right edge of the Scroll Container's Rect is less than the right margin
auto rightDistance = size.width - rect.getRight();
cp.left = com::Bool { rightDistance < margins.right };

return cp;
}

com::Bool ScrollContainer::isScrollableHorziontal(f32 direction) const noexcept
{
auto cp = getScrollableCompass();
if(direction > 0.0f) return cp.right;
return cp.left;
}

com::Bool ScrollContainer::isScrollableVertical(f32 direction) const noexcept
{
auto cp = getScrollableCompass();
if(direction > 0.0f) return cp.bottom;
return cp.top;
}

void ScrollContainer::addScrollDelta(Vec2Df scrollDelta) noexcept
{
std::vector<Container*> childs = getChilds();
for(Container* child : childs)
{
Vec2Df position = child->getPosition();
Vec2Df br = child->getRect().getBottomRight();
Vec2Df tl = child->getRect().getTopLeft();
if((scrollDelta.y + tl.y) > 0.0f)
scrollDelta.y = -tl.y;
else if((scrollDelta.y + br.y) < 0.0f)
scrollDelta.y = 0.0f;
if((scrollDelta.x + tl.x) > 0.0f)
scrollDelta.x = -tl.x;
else if((scrollDelta.x + br.x) < 0.0f)
scrollDelta.x = 0.0f;
m_scrollDelta += scrollDelta;
child->setPosition(scrollDelta + position);
}
// If there is nothing to scroll (no childs) then do nothing
if(!m_scrollable)
return;

auto size = getSize();

auto margins = m_scrollable->getScrollMargins();
auto rect = m_scrollable->getContainer()->getRect();
Vec2Df position = rect.getPosition();

// Local coordinates of top, bottom, left, and right sides in their respective ordinate or abscissa
auto top = rect.getTop();
auto bottom = rect.getBottom();
auto left = rect.getLeft();
auto right = rect.getRight();

// Constrain Vertical Motion
// If scrolling downwards and the top distance is greater than the top scroll margin, then constrain the scroll
if(auto topDistance = top + scrollDelta.y; topDistance > margins.top && scrollDelta.y > 0)
scrollDelta.y = margins.top - top;
// If scrolling upwards and the bottom distance is greater than the bottom scroll margin, then constrain the scroll
else if(auto bottomDistance = size.height - (bottom + scrollDelta.y); bottomDistance > margins.bottom && scrollDelta.y < 0)
scrollDelta.y = -(margins.bottom - (size.height - bottom));

// Constrain Horizontal Motion
// If scrolling towards right and the left distance is greater than the left scroll margin, then constrain the scroll
if(auto leftDistance = left + scrollDelta.x; leftDistance > margins.left && scrollDelta.x > 0)
scrollDelta.x = margins.left - left;
// If scrolling towards left and the right distance is greater than the right scroll margin, then constrain the scroll
else if(auto rightDistance = size.width - (right + scrollDelta.x); rightDistance > margins.right && scrollDelta.x < 0)
scrollDelta.x = -(margins.right - (size.width - right));

// Save the accrued scroll delta
m_scrollDelta += scrollDelta;
// Apply the scroll delta (new position)
m_scrollable->getContainer()->setPosition(scrollDelta + position);
}

void ScrollContainer::resetScrollDelta() noexcept
{
addScrollDelta(-getScrollDelta());
}
}
Loading

0 comments on commit 1f6128b

Please sign in to comment.