Feature/scroll indication (#3)

Add scroll arrows when there are more items in the list than the list can show at a time.

These arrows can be shown in the main menu (although there aren't enough items in there to have them shown) or in the various DistributionEventPokemonListScene menus.

I also did provisions to support the same for the ScrollWidget in the about screen. Although I haven't implemented that (yet?)
This commit is contained in:
Philippe Symons 2024-08-22 21:45:31 +02:00 committed by GitHub
parent bc7733cc36
commit 90bb9ccceb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 361 additions and 54 deletions

BIN
assets/ui-arrow-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
assets/ui-arrow-up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -3,13 +3,13 @@
#include <libdragon.h>
enum class UINavigationKey
enum class UINavigationDirection
{
NONE,
UP,
RIGHT,
DOWN,
LEFT
LEFT,
MAX
};
enum class NavigationInputSourceType
@ -24,6 +24,6 @@ enum class NavigationInputSourceType
* This function determines whether the joypad_inputs_t has analog or dpad positions/presses that could be considered for UI navigation.
* If so, it will return the most prominent direction.
*/
const UINavigationKey determineUINavigationKey(joypad_inputs_t inputs, NavigationInputSourceType sourceType);
const UINavigationDirection determineUINavigationDirection(joypad_inputs_t inputs, NavigationInputSourceType sourceType);
#endif

View File

@ -3,11 +3,13 @@
#include "scenes/SceneWithDialogWidget.h"
#include "widget/VerticalList.h"
#include "widget/ImageWidget.h"
#include "widget/DialogWidget.h"
#include "widget/CursorWidget.h"
#include "widget/MenuItemWidget.h"
#include "widget/ListItemFiller.h"
#include "widget/IFocusListener.h"
#include "widget/IScrollWindowListener.h"
typedef struct MenuSceneContext
{
@ -20,7 +22,7 @@ typedef struct MenuSceneContext
* @brief A scene showing a menu
*
*/
class MenuScene : public SceneWithDialogWidget, public IFocusListener
class MenuScene : public SceneWithDialogWidget, public IFocusListener, public IScrollWindowListener
{
public:
MenuScene(SceneDependencies& deps, void* context);
@ -36,6 +38,7 @@ public:
virtual void onDialogDone();
void focusChanged(const FocusChangeStatus& status) override;
void onScrollWindowChanged(const ScrollWindowUpdate& update) override;
SceneDependencies& getDependencies();
@ -47,7 +50,11 @@ protected:
MenuSceneContext* context_;
sprite_t* menu9SliceSprite_;
sprite_t* cursorSprite_;
sprite_t* uiArrowUpSprite_;
sprite_t* uiArrowDownSprite_;
VerticalList menuList_;
ImageWidget scrollArrowUp_;
ImageWidget scrollArrowDown_;
CursorWidget cursorWidget_;
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> menuListFiller_;
WidgetFocusChainSegment listFocusChainSegment_;

View File

@ -0,0 +1,54 @@
#ifndef ISCROLLWINDOWLISTENER_H
#define ISCROLLWINDOWLISTENER_H
#include "core/common.h"
#include "core/DragonUtils.h"
/**
* This struct contains the various things to know about the scroll window
* when it has been changed.
*/
typedef struct ScrollWindowUpdate
{
/**
* The coords and size of the scroll window relative to the widget
* that implements the scrolling
*/
Rectangle scrollWindowRectangle;
/**
* The total size of the panel containing all of the widgets in the scrolling widget.
* this includes the parts that aren't visible (because they're not in the scroll window)
*/
Dimensions totalSize;
} ScrollWindowUpdate;
bool canScrollTo(const ScrollWindowUpdate& info, UINavigationDirection direction);
/**
* This class allows you to react to changes to the scroll window of a widget.
* This is useful for showing UI indications like scrollbars or "more" arrows
*/
class IScrollWindowListener
{
public:
virtual ~IScrollWindowListener();
virtual void onScrollWindowChanged(const ScrollWindowUpdate& update) = 0;
protected:
private:
};
/**
* Test implementation that will just print the scroll window update
*/
class TestScrollWindowListener : public IScrollWindowListener
{
public:
virtual ~TestScrollWindowListener();
void onScrollWindowChanged(const ScrollWindowUpdate& update) override;
protected:
private:
};
#endif

View File

@ -5,11 +5,13 @@
#include "animations/IAnimation.h"
#include <vector>
typedef std::vector<IWidget*> WidgetList;
class IScrollWindowListener;
class ScrollWidget;
class AnimationManager;
typedef std::vector<IWidget*> WidgetList;
typedef std::vector<IScrollWindowListener*> ScrollWindowListenerList;
typedef struct ScrollWidgetStyle
{
// it defines the amount of pixels we should scroll when the analog stick or dpad is used.
@ -84,16 +86,8 @@ public:
*/
void setWindowStart(const Point& windowStart);
/**
* @brief Indicates the percentage at which the current window position is at horizontally
* in relation to the entire window
*/
float getWindowProgressX() const;
/**
* @brief Indicates the percentage at which the current window position is at vertically
* in relation to the entire window
*/
float getWindowProgressY() const;
void registerScrollWindowListener(IScrollWindowListener* listener);
void unregisterScrollWindowListener(IScrollWindowListener* listener);
protected:
private:
/**
@ -107,8 +101,11 @@ private:
*/
void recalculateWindowSize();
void notifyScrollWindowListeners();
MoveScrollWidgetWindowAnimation windowAnimation_;
WidgetList widgets_;
ScrollWindowListenerList scrollWindowListeners_;
ScrollWidgetStyle style_;
AnimationManager& animManager_;
Rectangle bounds_;

View File

@ -11,13 +11,14 @@ class RDPQGraphics;
class IWidget;
class VerticalList;
class AnimationManager;
struct FocusChangeStatus;
class IFocusListener;
class IScrollWindowListener;
typedef std::vector<IWidget*> IWidgetList;
typedef std::vector<Rectangle> WidgetBoundsList;
struct FocusChangeStatus;
class IFocusListener;
typedef std::vector<IFocusListener*> FocusListenerList;
typedef std::vector<IScrollWindowListener*> ScrollWindowListenerList;
/**
* @brief This Animation implementation is used internal in VerticalList
@ -138,6 +139,7 @@ public:
bool focusPrevious();
void addWidget(IWidget* widget);
void removeWidget(IWidget* widget);
void clearWidgets();
VerticalListStyle& getStyle();
@ -160,6 +162,10 @@ public:
void registerFocusListener(IFocusListener* listener);
void unregisterFocusListener(IFocusListener* listener);
void registerScrollWindowListener(IScrollWindowListener* listener);
void unregisterScrollWindowListener(IScrollWindowListener* listener);
void notifyScrollWindowListeners();
protected:
private:
void rebuildLayout();
@ -176,6 +182,7 @@ private:
IWidgetList widgetList_;
WidgetBoundsList widgetBoundsList_;
FocusListenerList focusListeners_;
ScrollWindowListenerList scrollWindowListeners_;
VerticalListStyle listStyle_;
Rectangle bounds_;
uint32_t windowMinY_;

View File

@ -2,7 +2,7 @@
static uint8_t ANALOG_STICK_THRESHOLD = 30;
const UINavigationKey determineUINavigationKey(joypad_inputs_t inputs, NavigationInputSourceType sourceType)
const UINavigationDirection determineUINavigationDirection(joypad_inputs_t inputs, NavigationInputSourceType sourceType)
{
if(sourceType == NavigationInputSourceType::ANALOG_STICK || sourceType == NavigationInputSourceType::BOTH)
{
@ -13,14 +13,14 @@ const UINavigationKey determineUINavigationKey(joypad_inputs_t inputs, Navigatio
{
if(absXVal >= ANALOG_STICK_THRESHOLD)
{
return (inputs.stick_x < 0) ? UINavigationKey::LEFT : UINavigationKey::RIGHT;
return (inputs.stick_x < 0) ? UINavigationDirection::LEFT : UINavigationDirection::RIGHT;
}
}
else
{
if(absYVal >= ANALOG_STICK_THRESHOLD)
{
return (inputs.stick_y < 0) ? UINavigationKey::DOWN : UINavigationKey::UP;
return (inputs.stick_y < 0) ? UINavigationDirection::DOWN : UINavigationDirection::UP;
}
}
}
@ -29,20 +29,20 @@ const UINavigationKey determineUINavigationKey(joypad_inputs_t inputs, Navigatio
{
if(inputs.btn.d_down)
{
return UINavigationKey::DOWN;
return UINavigationDirection::DOWN;
}
if(inputs.btn.d_up)
{
return UINavigationKey::UP;
return UINavigationDirection::UP;
}
if(inputs.btn.d_left)
{
return UINavigationKey::LEFT;
return UINavigationDirection::LEFT;
}
if(inputs.btn.d_right)
{
return UINavigationKey::RIGHT;
return UINavigationDirection::RIGHT;
}
}
return UINavigationKey::NONE;
return UINavigationDirection::MAX;
}

View File

@ -44,23 +44,23 @@ bool AbstractUIScene::handleUserInput(joypad_port_t port, const joypad_inputs_t&
{
// the widget did not handle the userInput. If we're dealing with a navigation key, we may want to switch focus
WidgetFocusChainSegment* nextChainEntry;
const UINavigationKey navKey = determineUINavigationKey(inputs, NavigationInputSourceType::BOTH);
const UINavigationDirection navDirection = determineUINavigationDirection(inputs, NavigationInputSourceType::BOTH);
switch(navKey)
switch(navDirection)
{
case UINavigationKey::UP:
case UINavigationDirection::UP:
nextChainEntry = focusChain_->onUp;
break;
case UINavigationKey::DOWN:
case UINavigationDirection::DOWN:
nextChainEntry = focusChain_->onDown;
break;
case UINavigationKey::LEFT:
case UINavigationDirection::LEFT:
nextChainEntry = focusChain_->onLeft;
break;
case UINavigationKey::RIGHT:
case UINavigationDirection::RIGHT:
nextChainEntry = focusChain_->onRight;
break;
case UINavigationKey::NONE:
case UINavigationDirection::MAX:
default:
nextChainEntry = nullptr;
break;

View File

@ -3,6 +3,10 @@
#include "scenes/StatsScene.h"
#include "transferpak/TransferPakManager.h"
static const Rectangle menuListBounds = {20, 20, 280, 0};
static const Rectangle imgScrollArrowUpBounds = {.x = 154, .y = 14, .width = 11, .height = 6};
static const Rectangle imgScrollArrowDownBounds = {.x = 154, .y = 220, .width = 11, .height = 6};
static DistributionPokemonListSceneContext* convert(void* context)
{
return static_cast<DistributionPokemonListSceneContext*>(context);
@ -154,12 +158,17 @@ void DistributionPokemonListScene::setupMenu()
},
.margin = {
.top = 5
},
.autogrow = {
.enabled = true,
.maxHeight = 200
}
};
menuList_.setStyle(listStyle);
menuList_.setBounds(Rectangle{20, 20, 280, 150});
menuList_.setBounds(menuListBounds);
menuList_.setVisible(true);
menuList_.registerScrollWindowListener(this);
cursorWidget_.setVisible(false);
@ -178,6 +187,28 @@ void DistributionPokemonListScene::setupMenu()
};
menuListFiller_.addItems(context_->menuEntries, context_->numMenuEntries, itemStyle);
const ImageWidgetStyle scrollArrowUpStyle = {
.image = {
.sprite = uiArrowUpSprite_,
.spriteBounds = {0, 0, imgScrollArrowUpBounds.width, imgScrollArrowUpBounds.height}
}
};
scrollArrowUp_.setStyle(scrollArrowUpStyle);
scrollArrowUp_.setBounds(imgScrollArrowUpBounds);
const ImageWidgetStyle scrollArrowDownStyle = {
.image = {
.sprite = uiArrowDownSprite_,
.spriteBounds = { 0, 0, imgScrollArrowDownBounds.width, imgScrollArrowDownBounds.height}
}
};
// note: even though autogrow is turned on for the vertical list, it doesn't matter for the down arrow.
// because when the list is still growing, no scrolling is needed anyway, so the arrow would be invisible anyway.
scrollArrowDown_.setStyle(scrollArrowDownStyle);
scrollArrowDown_.setBounds(imgScrollArrowDownBounds);
}
void DistributionPokemonListScene::loadDistributionPokemonList()

View File

@ -5,6 +5,11 @@
#include "menu/MenuFunctions.h"
#include <cstdio>
#include <cmath>
static const Rectangle menuListBounds = {100, 30, 150, 0};
static const Rectangle imgScrollArrowUpBounds = {.x = 170, .y = 24, .width = 11, .height = 6};
static const Rectangle imgScrollArrowDownBounds = {.x = 170, .y = 180, .width = 11, .height = 6};
static void dialogFinishedCallback(void* context)
{
@ -17,7 +22,11 @@ MenuScene::MenuScene(SceneDependencies& deps, void* context)
, context_(static_cast<MenuSceneContext*>(context))
, menu9SliceSprite_(nullptr)
, cursorSprite_(nullptr)
, uiArrowUpSprite_(nullptr)
, uiArrowDownSprite_(nullptr)
, menuList_(deps.animationManager)
, scrollArrowUp_()
, scrollArrowDown_()
, cursorWidget_(deps.animationManager)
, menuListFiller_(menuList_)
, listFocusChainSegment_(WidgetFocusChainSegment{
@ -47,6 +56,8 @@ void MenuScene::init()
// load these sprites before the parent init because setupDialog(style) will need them
menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite");
cursorSprite_ = sprite_load("rom://hand-cursor.sprite");
uiArrowUpSprite_ = sprite_load("rom://ui-arrow-up.sprite");
uiArrowDownSprite_ = sprite_load("rom://ui-arrow-down.sprite");
SceneWithDialogWidget::init();
@ -58,16 +69,25 @@ void MenuScene::init()
void MenuScene::destroy()
{
menuList_.unregisterFocusListener(this);
menuList_.unregisterScrollWindowListener(this);
menuList_.clearWidgets();
menuList_.setStyle({0});
cursorWidget_.setStyle({0});
scrollArrowUp_.setStyle({0});
scrollArrowDown_.setStyle({0});
// destroy the parent before releasing the sprites because the dialog widget
// may still have a reference to them
SceneWithDialogWidget::destroy();
sprite_free(uiArrowDownSprite_);
uiArrowDownSprite_ = nullptr;
sprite_free(uiArrowUpSprite_);
uiArrowUpSprite_ = nullptr;
sprite_free(cursorSprite_);
cursorSprite_ = nullptr;
sprite_free(menu9SliceSprite_);
menu9SliceSprite_ = nullptr;
}
void MenuScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds)
@ -75,12 +95,8 @@ void MenuScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds)
menuList_.render(gfx, sceneBounds);
cursorWidget_.render(gfx, sceneBounds);
SceneWithDialogWidget::render(gfx, sceneBounds);
TextRenderSettings renderSettings = {
.fontId = arialId_,
.fontStyleId = fontStyleWhiteId_
};
gfx.drawText(Rectangle{40, 10, 280, 16}, "PokeMe64 by risingPhil. Version 0.1", renderSettings);
scrollArrowUp_.render(gfx, sceneBounds);
scrollArrowDown_.render(gfx, sceneBounds);
}
bool MenuScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs)
@ -141,6 +157,12 @@ void MenuScene::focusChanged(const FocusChangeStatus& status)
cursorWidget_.moveToBounds(newCursorBounds);
}
void MenuScene::onScrollWindowChanged(const ScrollWindowUpdate& update)
{
scrollArrowUp_.setVisible(canScrollTo(update, UINavigationDirection::UP));
scrollArrowDown_.setVisible(canScrollTo(update, UINavigationDirection::DOWN));
}
SceneDependencies& MenuScene::getDependencies()
{
return deps_;
@ -173,11 +195,12 @@ void MenuScene::setupMenu()
};
menuList_.setStyle(listStyle);
menuList_.setBounds(Rectangle{100, 30, 150, 0});
menuList_.setBounds(menuListBounds);
menuList_.setVisible(true);
cursorWidget_.setStyle(cursorStyle);
cursorWidget_.setVisible(true);
menuList_.registerFocusListener(this);
menuList_.registerScrollWindowListener(this);
const MenuItemStyle itemStyle = {
.size = {150, 16},
@ -202,6 +225,28 @@ void MenuScene::setupMenu()
}
menuListFiller_.addItems(context_->menuEntries, context_->numMenuEntries, itemStyle);
const ImageWidgetStyle scrollArrowUpStyle = {
.image = {
.sprite = uiArrowUpSprite_,
.spriteBounds = {0, 0, imgScrollArrowUpBounds.width, imgScrollArrowUpBounds.height}
}
};
scrollArrowUp_.setStyle(scrollArrowUpStyle);
scrollArrowUp_.setBounds(imgScrollArrowUpBounds);
const ImageWidgetStyle scrollArrowDownStyle = {
.image = {
.sprite = uiArrowDownSprite_,
.spriteBounds = { 0, 0, imgScrollArrowDownBounds.width, imgScrollArrowDownBounds.height}
}
};
// note: even though autogrow is turned on for the vertical list, it doesn't matter for the down arrow.
// because when the list is still growing, no scrolling is needed anyway, so the arrow would be invisible anyway.
scrollArrowDown_.setStyle(scrollArrowDownStyle);
scrollArrowDown_.setBounds(imgScrollArrowDownBounds);
}
void MenuScene::setupDialog(DialogWidgetStyle& style)

View File

@ -0,0 +1,37 @@
#include "widget/IScrollWindowListener.h"
#include <libdragon.h>
bool canScrollTo(const ScrollWindowUpdate& info, UINavigationDirection direction)
{
switch(direction)
{
case UINavigationDirection::UP:
return (info.scrollWindowRectangle.y > 0);
case UINavigationDirection::RIGHT:
return (info.scrollWindowRectangle.x + info.scrollWindowRectangle.width < info.totalSize.width);
case UINavigationDirection::DOWN:
return (info.scrollWindowRectangle.y + info.scrollWindowRectangle.height < info.totalSize.height);
case UINavigationDirection::LEFT:
return (info.scrollWindowRectangle.x > 0);
default:
return false;
}
}
IScrollWindowListener::~IScrollWindowListener()
{
}
TestScrollWindowListener::~TestScrollWindowListener()
{
}
void TestScrollWindowListener::onScrollWindowChanged(const ScrollWindowUpdate &update)
{
bool canScroll[4];
for(int i=0; i < 4; ++i)
{
canScroll[i] = canScrollTo(update, static_cast<UINavigationDirection>(i));
}
debugf("[TestScrollWindowListener]:%s:%p: scrollWindowRectangle: [%d, %d, %d, %d], totalSize: [%d, %d], scrollUp: %d, scrollRight %d, scrollDown %d, scrollLeft %d\r\n", __FUNCTION__, this, update.scrollWindowRectangle.x, update.scrollWindowRectangle.y, update.scrollWindowRectangle.width, update.scrollWindowRectangle.height, update.totalSize.width, update.totalSize.height, canScroll[0], canScroll[1], canScroll[2], canScroll[3]);
}

View File

@ -1,4 +1,5 @@
#include "widget/ScrollWidget.h"
#include "widget/IScrollWindowListener.h"
#include "core/common.h"
#include "scenes/AbstractUIScene.h"
#include "animations/AnimationManager.h"
@ -135,6 +136,7 @@ void MoveScrollWidgetWindowAnimation::apply(float pos)
ScrollWidget::ScrollWidget(AnimationManager& animManager)
: windowAnimation_(this)
, widgets_()
, scrollWindowListeners_()
, style_({0})
, animManager_(animManager)
, bounds_({0})
@ -194,6 +196,7 @@ void ScrollWidget::addWidget(IWidget* widget)
{
widgets_.push_back(widget);
growWindow(widget);
notifyScrollWindowListeners();
}
void ScrollWidget::removeWidget(IWidget* widget)
@ -210,6 +213,7 @@ void ScrollWidget::clearWidgets()
{
widgets_.clear();
windowBounds_ = {0};
notifyScrollWindowListeners();
}
bool ScrollWidget::handleUserInput(const joypad_inputs_t& userInput)
@ -297,18 +301,22 @@ void ScrollWidget::setWindowStart(const Point& windowStart)
{
windowBounds_.x = windowStart.x;
windowBounds_.y = windowStart.y;
notifyScrollWindowListeners();
}
float ScrollWidget::getWindowProgressX() const
void ScrollWidget::registerScrollWindowListener(IScrollWindowListener* listener)
{
const float maxX = windowBounds_.width - bounds_.width;
return static_cast<float>(windowBounds_.x) / maxX;
scrollWindowListeners_.push_back(listener);
}
float ScrollWidget::getWindowProgressY() const
void ScrollWidget::unregisterScrollWindowListener(IScrollWindowListener* listener)
{
const double maxY = windowBounds_.height - bounds_.height;
return static_cast<float>(windowBounds_.y) / maxY;
auto it = std::find(scrollWindowListeners_.begin(), scrollWindowListeners_.end(), listener);
if(it != scrollWindowListeners_.end())
{
scrollWindowListeners_.erase(it);
}
}
void ScrollWidget::growWindow(IWidget* widget)
@ -336,4 +344,18 @@ void ScrollWidget::recalculateWindowSize()
{
growWindow(widget);
}
notifyScrollWindowListeners();
}
void ScrollWidget::notifyScrollWindowListeners()
{
const ScrollWindowUpdate update = {
.scrollWindowRectangle = {.x = windowBounds_.x, .y = windowBounds_.y, .width = bounds_.width, .height = bounds_.height},
.totalSize = {.width = windowBounds_.width, .height = windowBounds_.height}
};
for(IScrollWindowListener* listener : scrollWindowListeners_)
{
listener->onScrollWindowChanged(update);
}
}

View File

@ -1,6 +1,7 @@
#include "widget/VerticalList.h"
#include "widget/IWidget.h"
#include "widget/IFocusListener.h"
#include "widget/IScrollWindowListener.h"
#include "core/RDPQGraphics.h"
#include "animations/AnimationManager.h"
#include "core/DragonUtils.h"
@ -105,6 +106,7 @@ VerticalList::VerticalList(AnimationManager& animationManager)
, widgetList_()
, widgetBoundsList_()
, focusListeners_()
, scrollWindowListeners_()
, listStyle_({0})
, bounds_({0})
, windowMinY_(0)
@ -195,10 +197,48 @@ void VerticalList::addWidget(IWidget *widget)
}
else
{
const Rectangle lastWidgetBounds = widgetBoundsList_.back();
const Rectangle& lastWidgetBounds = widgetBoundsList_.back();
widgetBoundsList_.push_back(Rectangle{.x = 0, .y = lastWidgetBounds.y + lastWidgetBounds.height + listStyle_.verticalSpacingBetweenWidgets, .width = widgetSize.width, .height = widgetSize.height});
}
autoGrowBounds();
notifyScrollWindowListeners();
}
void VerticalList::removeWidget(IWidget* widget)
{
const auto listSize = widgetList_.size();
for(uint16_t i = 0; i < listSize; ++i)
{
if(widgetList_[i] != widget)
{
continue;
}
//found the widget
// if the widget -that we're about to remove- is focused, we need to move the focus to a different widget
if(widgetList_[i]->isFocused() && listSize > 1)
{
// if the to-be-removed widget is the last one in the list, we need to focus the one before it.
// if it's not, we'll focus the next one
uint16_t newFocusIndex;
if(i < (listSize - 1))
{
newFocusIndex = i + 1;
// the next list item will move to the current position after the erase() call
focusedWidgetIndex_ = i;
}
else
{
newFocusIndex = i - 1;
focusedWidgetIndex_ = newFocusIndex;
}
widgetList_[newFocusIndex]->setFocused(true);
}
widgetList_.erase(widgetList_.begin() + i);
widgetBoundsList_.erase(widgetBoundsList_.begin() + i);
rebuildLayout();
break;
}
}
void VerticalList::clearWidgets()
@ -206,6 +246,7 @@ void VerticalList::clearWidgets()
widgetList_.clear();
widgetBoundsList_.clear();
focusedWidgetIndex_ = 0;
notifyScrollWindowListeners();
}
VerticalListStyle& VerticalList::getStyle()
@ -222,6 +263,7 @@ void VerticalList::setStyle(const VerticalListStyle& style)
void VerticalList::setViewWindowStartY(uint32_t windowStartY)
{
windowMinY_ = windowStartY;
notifyScrollWindowListeners();
}
bool VerticalList::isFocused() const
@ -296,9 +338,9 @@ bool VerticalList::handleUserInput(const joypad_inputs_t& userInput)
return hasFocusedWidgetHandledInput;
}
const UINavigationKey navKey = determineUINavigationKey(userInput, NavigationInputSourceType::BOTH);
const UINavigationDirection navDirection = determineUINavigationDirection(userInput, NavigationInputSourceType::BOTH);
if(navKey == UINavigationKey::UP)
if(navDirection == UINavigationDirection::UP)
{
if(focusedWidgetIndex_ < 1)
{
@ -306,7 +348,7 @@ bool VerticalList::handleUserInput(const joypad_inputs_t& userInput)
}
return focusPrevious();
}
else if(navKey == UINavigationKey::DOWN)
else if(navDirection == UINavigationDirection::DOWN)
{
if(focusedWidgetIndex_ == widgetList_.size() - 1)
{
@ -399,8 +441,52 @@ void VerticalList::unregisterFocusListener(IFocusListener* focusListener)
}
}
void VerticalList::registerScrollWindowListener(IScrollWindowListener* listener)
{
scrollWindowListeners_.push_back(listener);
}
void VerticalList::unregisterScrollWindowListener(IScrollWindowListener* listener)
{
auto it = std::find(scrollWindowListeners_.begin(), scrollWindowListeners_.end(), listener);
if(it != scrollWindowListeners_.end())
{
scrollWindowListeners_.erase(it);
}
}
void VerticalList::notifyScrollWindowListeners()
{
int totalHeight;
if(widgetList_.empty())
{
totalHeight = 0;
}
else
{
const Rectangle& lastWidgetBounds = widgetBoundsList_.back();
totalHeight = lastWidgetBounds.y + lastWidgetBounds.height;
}
// calculate the size of the scroll panel with the margins subtracted
const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.margin.top, listStyle_.margin.bottom);
const ScrollWindowUpdate update = {
.scrollWindowRectangle = {.x = 0, .y = static_cast<int>(windowMinY_), .width = bounds_.width, .height = static_cast<int>(innerListHeight)},
.totalSize = {.width = bounds_.width, .height = totalHeight}
};
for(IScrollWindowListener* listener : scrollWindowListeners_)
{
listener->onScrollWindowChanged(update);
}
}
void VerticalList::rebuildLayout()
{
Rectangle focusedWidgetBounds;
IWidget* focusedWidget;
int lastWidgetEndY = 0;
if(widgetList_.empty())
{
@ -415,6 +501,27 @@ void VerticalList::rebuildLayout()
widgetBoundsList_[i] = {.x = 0, .y = lastWidgetEndY, .width = widgetSize.width, .height = widgetSize.height};
lastWidgetEndY += widgetSize.height + listStyle_.verticalSpacingBetweenWidgets;
}
if(!widgetList_.empty())
{
focusedWidgetBounds = widgetBoundsList_[focusedWidgetIndex_];
focusedWidget = widgetList_[focusedWidgetIndex_];
}
else
{
// HACK: safety net in case the list is empty (to avoid an out-of-bounds list access crash)
// the use of windowMinY_ here is to compensate for what calculateListWidgetBounds is calculating
focusedWidgetBounds = Rectangle{.x = 0, .y = static_cast<int>(windowMinY_), .width = 0, .height = 0};
focusedWidget = nullptr;
}
const FocusChangeStatus changeStatus = {
.focusBounds = calculateListWidgetBounds(focusedWidgetBounds, windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top),
.curFocus = focusedWidget
};
notifyFocusListeners(changeStatus);
notifyScrollWindowListeners();
}
int32_t VerticalList::scrollWindowToFocusedWidget()
@ -447,7 +554,7 @@ void VerticalList::notifyFocusListeners(const FocusChangeStatus& status)
void VerticalList::autoGrowBounds()
{
if(!listStyle_.autogrow.enabled)
if(!listStyle_.autogrow.enabled || widgetList_.empty())
{
return;
}