diff --git a/assets/ui-arrow-down.png b/assets/ui-arrow-down.png new file mode 100644 index 0000000..b2c853a Binary files /dev/null and b/assets/ui-arrow-down.png differ diff --git a/assets/ui-arrow-up.png b/assets/ui-arrow-up.png new file mode 100644 index 0000000..dc377a6 Binary files /dev/null and b/assets/ui-arrow-up.png differ diff --git a/include/core/DragonUtils.h b/include/core/DragonUtils.h index 0b87ef0..cbb55d5 100755 --- a/include/core/DragonUtils.h +++ b/include/core/DragonUtils.h @@ -3,13 +3,13 @@ #include -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 \ No newline at end of file diff --git a/include/scenes/MenuScene.h b/include/scenes/MenuScene.h index 202c18b..53e7b16 100755 --- a/include/scenes/MenuScene.h +++ b/include/scenes/MenuScene.h @@ -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 menuListFiller_; WidgetFocusChainSegment listFocusChainSegment_; diff --git a/include/widget/IScrollWindowListener.h b/include/widget/IScrollWindowListener.h new file mode 100644 index 0000000..746ba91 --- /dev/null +++ b/include/widget/IScrollWindowListener.h @@ -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 \ No newline at end of file diff --git a/include/widget/ScrollWidget.h b/include/widget/ScrollWidget.h index 22fbbe2..6476a06 100644 --- a/include/widget/ScrollWidget.h +++ b/include/widget/ScrollWidget.h @@ -5,11 +5,13 @@ #include "animations/IAnimation.h" #include -typedef std::vector WidgetList; - +class IScrollWindowListener; class ScrollWidget; class AnimationManager; +typedef std::vector WidgetList; +typedef std::vector 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_; diff --git a/include/widget/VerticalList.h b/include/widget/VerticalList.h index 8874eb8..488b19c 100755 --- a/include/widget/VerticalList.h +++ b/include/widget/VerticalList.h @@ -11,13 +11,14 @@ class RDPQGraphics; class IWidget; class VerticalList; class AnimationManager; +struct FocusChangeStatus; +class IFocusListener; +class IScrollWindowListener; typedef std::vector IWidgetList; typedef std::vector WidgetBoundsList; - -struct FocusChangeStatus; -class IFocusListener; typedef std::vector FocusListenerList; +typedef std::vector 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_; diff --git a/src/core/DragonUtils.cpp b/src/core/DragonUtils.cpp index c62f083..c5840f0 100755 --- a/src/core/DragonUtils.cpp +++ b/src/core/DragonUtils.cpp @@ -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; } \ No newline at end of file diff --git a/src/scenes/AbstractUIScene.cpp b/src/scenes/AbstractUIScene.cpp index 633fd24..c457ab1 100755 --- a/src/scenes/AbstractUIScene.cpp +++ b/src/scenes/AbstractUIScene.cpp @@ -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; diff --git a/src/scenes/DistributionPokemonListScene.cpp b/src/scenes/DistributionPokemonListScene.cpp index c075364..4bb248e 100755 --- a/src/scenes/DistributionPokemonListScene.cpp +++ b/src/scenes/DistributionPokemonListScene.cpp @@ -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(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() diff --git a/src/scenes/MenuScene.cpp b/src/scenes/MenuScene.cpp index fcee981..7972537 100755 --- a/src/scenes/MenuScene.cpp +++ b/src/scenes/MenuScene.cpp @@ -5,6 +5,11 @@ #include "menu/MenuFunctions.h" #include +#include + +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(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) diff --git a/src/widget/IScrollWindowListener.cpp b/src/widget/IScrollWindowListener.cpp new file mode 100644 index 0000000..e287186 --- /dev/null +++ b/src/widget/IScrollWindowListener.cpp @@ -0,0 +1,37 @@ +#include "widget/IScrollWindowListener.h" +#include + +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(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]); +} \ No newline at end of file diff --git a/src/widget/ScrollWidget.cpp b/src/widget/ScrollWidget.cpp index b693502..218ea1c 100644 --- a/src/widget/ScrollWidget.cpp +++ b/src/widget/ScrollWidget.cpp @@ -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(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(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); + } } \ No newline at end of file diff --git a/src/widget/VerticalList.cpp b/src/widget/VerticalList.cpp index 3b014ee..c3eb769 100755 --- a/src/widget/VerticalList.cpp +++ b/src/widget/VerticalList.cpp @@ -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(windowMinY_), .width = bounds_.width, .height = static_cast(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(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; }