~aleteoryx/muditaos

8f801262510042a71c5c2ef8b2ed1391995d8f7d — Przemyslaw Brudny 5 years ago 7b50138
[EGD-3434] ListView Scroll bar refactor

Added two new scroll bar types. Fixed for ListView
with equal height elements and PreRendered which
require whole list render - not recommended for big
lists but works with not equal heights elements.
Applied new types to all lists in Pure. Various
cleanups and refactors inside models and listView.
28 files changed, 399 insertions(+), 120 deletions(-)

M module-apps/DatabaseModel.hpp
M module-apps/InternalModel.hpp
M module-apps/application-alarm-clock/windows/AlarmClockMainWindow.cpp
M module-apps/application-alarm-clock/windows/CustomRepeatWindow.cpp
M module-apps/application-alarm-clock/windows/NewEditAlarmWindow.cpp
M module-apps/application-calendar/windows/CustomRepeatWindow.cpp
M module-apps/application-calendar/windows/DayEventsWindow.cpp
M module-apps/application-calendar/windows/EventDetailWindow.cpp
M module-apps/application-calendar/windows/NewEditEventWindow.cpp
M module-apps/application-calllog/windows/CallLogMainWindow.cpp
M module-apps/application-meditation/windows/MeditationListViewWindows.cpp
M module-apps/application-messages/windows/MessagesMainWindow.cpp
M module-apps/application-messages/windows/SMSTemplatesWindow.cpp
M module-apps/application-messages/windows/SMSThreadViewWindow.cpp
M module-apps/application-messages/windows/SearchResults.cpp
M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp
M module-apps/application-notes/windows/NoteMainWindow.cpp
M module-apps/application-notes/windows/SearchResultsWindow.cpp
M module-apps/application-phonebook/windows/PhonebookContactDetails.cpp
M module-apps/application-phonebook/windows/PhonebookIceContacts.cpp
M module-apps/application-phonebook/windows/PhonebookNewContact.cpp
M module-gui/gui/widgets/BoxLayout.cpp
M module-gui/gui/widgets/BoxLayout.hpp
M module-gui/gui/widgets/ListItemProvider.hpp
M module-gui/gui/widgets/ListView.cpp
M module-gui/gui/widgets/ListView.hpp
M module-gui/gui/widgets/Style.hpp
M module-gui/test/test-google/test-gui-listview.cpp
M module-apps/DatabaseModel.hpp => module-apps/DatabaseModel.hpp +23 -16
@@ 1,4 1,4 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 17,14 17,13 @@ namespace app
    template <class T> class DatabaseModel
    {
      protected:
        /// Pointer to application that owns the model
        Application *application = nullptr;
        uint32_t recordsCount    = 0;
        Application *application  = nullptr;
        unsigned int recordsCount = 0;
        int modelIndex            = 0;
        std::vector<std::shared_ptr<T>> records;
        uint32_t modelIndex = 0;

      public:
        DatabaseModel(Application *app) : application{app}, recordsCount{0}
        explicit DatabaseModel(Application *app) : application{app}, recordsCount{0}
        {}

        virtual ~DatabaseModel()


@@ 50,7 49,7 @@ namespace app
            }
        }

        virtual void clear()
        void clear()
        {
            records.clear();
            recordsCount = 0;


@@ 58,21 57,29 @@ namespace app

        std::shared_ptr<T> getRecord(gui::Order order)
        {
            auto index = modelIndex;
            auto index = 0;
            if (order == gui::Order::Next) {
                index = modelIndex;

            if (index >= records.size()) {
                return nullptr;
                modelIndex++;
            }
            else if (order == gui::Order::Previous) {
                index = records.size() - 1 + modelIndex;

            if (order == gui::Order::Previous) {
                index = records.size() - 1 - modelIndex;
                modelIndex--;
            }

            auto item = records[index];

            modelIndex++;
            if (isIndexValid(index)) {
                return records[index];
            }
            else {
                return nullptr;
            }
        }

            return item;
        [[nodiscard]] bool isIndexValid(unsigned int index) const noexcept
        {
            return index < records.size();
        }
    };


M module-apps/InternalModel.hpp => module-apps/InternalModel.hpp +16 -15
@@ 1,4 1,4 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 42,25 42,22 @@ namespace app

        gui::ListItem *getRecord(gui::Order order)
        {
            unsigned int index = 0;
            auto index = 0;
            if (order == gui::Order::Previous) {
                index = internalOffset + internalLimit - 1 - modelIndex;
                index = internalOffset + internalLimit - 1 + modelIndex;

                modelIndex--;
            }
            if (order == gui::Order::Next) {
                index = internalOffset + modelIndex;
            }

            if (isValidIndex(index, order)) {

                return getNextInternalDataElement(index);
                modelIndex++;
            }
            else {

                return nullptr;
            }
            return getInternalDataElement(index, order);
        }

        [[nodiscard]] bool isValidIndex(unsigned int index, gui::Order order) const
        [[nodiscard]] bool isIndexValid(unsigned int index, gui::Order order) const noexcept
        {
            return (index < internalData.size()) || (order == gui::Order::Previous && index < internalOffset);
        }


@@ 73,11 70,15 @@ namespace app
            Item->clearNavigationItem(gui::NavigationDirection::DOWN);
        }

        gui::ListItem *getNextInternalDataElement(unsigned int index)
        [[nodiscard]] gui::ListItem *getInternalDataElement(unsigned int index, gui::Order order)
        {
            modelIndex++;
            clearItemProperties(internalData[index]);
            return internalData[index];
            if (isIndexValid(index, order)) {
                clearItemProperties(internalData[index]);
                return internalData[index];
            }
            else {
                return nullptr;
            }
        }
    };


M module-apps/application-alarm-clock/windows/AlarmClockMainWindow.cpp => module-apps/application-alarm-clock/windows/AlarmClockMainWindow.cpp +2 -1
@@ 48,7 48,8 @@ namespace app::alarmClock
                                       style::alarmClock::window::listView_y,
                                       style::alarmClock::window::listView_w,
                                       style::alarmClock::window::listView_h,
                                       presenter->getAlarmsItemProvider());
                                       presenter->getAlarmsItemProvider(),
                                       style::listview::ScrollBarType::Fixed);
        alarmsList->focusChangedCallback = [this](gui::Item &) {
            onListFilled();
            return true;

M module-apps/application-alarm-clock/windows/CustomRepeatWindow.cpp => module-apps/application-alarm-clock/windows/CustomRepeatWindow.cpp +2 -1
@@ 29,7 29,8 @@ namespace app::alarmClock
                                 style::alarmClock::window::listView_y,
                                 style::alarmClock::window::listView_w,
                                 style::alarmClock::window::listView_h,
                                 presenter->getItemProvider());
                                 presenter->getItemProvider(),
                                 style::listview::ScrollBarType::None);
        setFocusItem(list);
    }


M module-apps/application-alarm-clock/windows/NewEditAlarmWindow.cpp => module-apps/application-alarm-clock/windows/NewEditAlarmWindow.cpp +2 -1
@@ 31,7 31,8 @@ namespace app::alarmClock
                                 style::alarmClock::window::listView_y,
                                 style::alarmClock::window::listView_w,
                                 style::alarmClock::window::listView_h,
                                 presenter->getAlarmsItemProvider());
                                 presenter->getAlarmsItemProvider(),
                                 style::listview::ScrollBarType::None);
        setFocusItem(list);
    }


M module-apps/application-calendar/windows/CustomRepeatWindow.cpp => module-apps/application-calendar/windows/CustomRepeatWindow.cpp +2 -1
@@ 39,7 39,8 @@ namespace gui
                                 style::window::calendar::listView_y,
                                 style::window::calendar::listView_w,
                                 style::window::calendar::listView_h,
                                 customRepeatModel);
                                 customRepeatModel,
                                 style::listview::ScrollBarType::None);
        setFocusItem(list);
    }


M module-apps/application-calendar/windows/DayEventsWindow.cpp => module-apps/application-calendar/windows/DayEventsWindow.cpp +4 -3
@@ 47,7 47,7 @@ namespace gui
            return false;
        }

        dayMonthTitle = item->getDayMonthText();
        dayMonthTitle   = item->getDayMonthText();
        filterFrom      = item->getDateFilter();
        auto filterTill = filterFrom + date::days{1};
        dayEventsModel->setFilters(filterFrom, filterTill, dayMonthTitle);


@@ 79,7 79,8 @@ namespace gui
                                          style::window::calendar::listView_y,
                                          style::window::calendar::listView_w,
                                          style::window::calendar::listView_h,
                                          dayEventsModel);
                                          dayEventsModel,
                                          style::listview::ScrollBarType::Fixed);
        setFocusItem(dayEventsList);
    }



@@ 101,7 102,7 @@ namespace gui
            rec->date_from = filterFrom;
            rec->date_till = filterFrom + std::chrono::hours(style::window::calendar::time::max_hour_24H_mode) +
                             std::chrono::minutes(style::window::calendar::time::max_minutes);
            auto event     = std::make_shared<EventsRecord>(*rec);
            auto event = std::make_shared<EventsRecord>(*rec);
            data->setData(event);
            application->switchWindow(
                style::window::calendar::name::new_edit_event, gui::ShowMode::GUI_SHOW_INIT, std::move(data));

M module-apps/application-calendar/windows/EventDetailWindow.cpp => module-apps/application-calendar/windows/EventDetailWindow.cpp +2 -1
@@ 38,7 38,8 @@ namespace gui
                                     style::window::calendar::listView_y,
                                     style::window::calendar::listView_w,
                                     style::window::calendar::listView_h,
                                     eventDetailModel);
                                     eventDetailModel,
                                     style::listview::ScrollBarType::PreRendered);

        setFocusItem(bodyList);
    }

M module-apps/application-calendar/windows/NewEditEventWindow.cpp => module-apps/application-calendar/windows/NewEditEventWindow.cpp +3 -2
@@ 35,7 35,8 @@ namespace gui
                                 style::window::calendar::listView_y,
                                 style::window::calendar::listView_w,
                                 style::window::calendar::listView_h,
                                 newEditEventModel);
                                 newEditEventModel,
                                 style::listview::ScrollBarType::PreRendered);
        setFocusItem(list);
    }



@@ 54,7 55,7 @@ namespace gui
        if (mode == ShowMode::GUI_SHOW_INIT) {
            auto rec = dynamic_cast<EventRecordData *>(data);
            if (rec != nullptr) {
                eventRecord    = rec->getData();
                eventRecord = rec->getData();
            }
            newEditEventModel->loadData(eventRecord);
        }

M module-apps/application-calllog/windows/CallLogMainWindow.cpp => module-apps/application-calllog/windows/CallLogMainWindow.cpp +7 -1
@@ 47,7 47,13 @@ namespace gui
        bottomBar->setText(BottomBar::Side::CENTER, utils::localize.get(style::strings::common::open));
        bottomBar->setText(BottomBar::Side::RIGHT, utils::localize.get(style::strings::common::back));

        list = new gui::ListView(this, mainWindow::x, mainWindow::y, mainWindow::w, mainWindow::h, calllogModel);
        list = new gui::ListView(this,
                                 mainWindow::x,
                                 mainWindow::y,
                                 mainWindow::w,
                                 mainWindow::h,
                                 calllogModel,
                                 style::listview::ScrollBarType::Fixed);

        setFocusItem(list);
    }

M module-apps/application-meditation/windows/MeditationListViewWindows.cpp => module-apps/application-meditation/windows/MeditationListViewWindows.cpp +7 -2
@@ 22,8 22,13 @@ void MeditationListViewWindow::buildInterface()
{
    AppWindow::buildInterface();
    model->createData();
    list = new gui::ListView(
        this, listViewWindow::X, listViewWindow::Y, listViewWindow::Width, listViewWindow::Height, model);
    list = new gui::ListView(this,
                             listViewWindow::X,
                             listViewWindow::Y,
                             listViewWindow::Width,
                             listViewWindow::Height,
                             model,
                             style::listview::ScrollBarType::Fixed);
    setFocusItem(list);
    bottomBar->setText(BottomBar::Side::RIGHT, utils::localize.get(style::strings::common::back));
}

M module-apps/application-messages/windows/MessagesMainWindow.cpp => module-apps/application-messages/windows/MessagesMainWindow.cpp +2 -1
@@ 53,7 53,8 @@ namespace gui
                                 msgThreadStyle::ListPositionY,
                                 msgThreadStyle::listWidth,
                                 msgThreadStyle::listHeight,
                                 threadsModel);
                                 threadsModel,
                                 style::listview::ScrollBarType::Fixed);
        list->setScrollTopMargin(style::margins::small);
        list->rebuildList();


M module-apps/application-messages/windows/SMSTemplatesWindow.cpp => module-apps/application-messages/windows/SMSTemplatesWindow.cpp +2 -1
@@ 46,7 46,8 @@ namespace gui

        namespace style = style::messages::templates::list;

        list = new gui::ListView(this, style::x, style::y, style::w, style::h, smsTemplateModel);
        list = new gui::ListView(
            this, style::x, style::y, style::w, style::h, smsTemplateModel, ::style::listview::ScrollBarType::Fixed);

        setFocusItem(list);
    }

M module-apps/application-messages/windows/SMSThreadViewWindow.cpp => module-apps/application-messages/windows/SMSThreadViewWindow.cpp +3 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SMSThreadViewWindow.hpp"


@@ 37,7 37,8 @@ namespace gui
                                    style::messages::smsList::y,
                                    style::messages::smsList::w,
                                    style::messages::smsList::h,
                                    smsModel);
                                    smsModel,
                                    style::listview::ScrollBarType::Proportional);
        smsList->setOrientation(style::listview::Orientation::BottomTop);

        setFocusItem(smsList);

M module-apps/application-messages/windows/SearchResults.cpp => module-apps/application-messages/windows/SearchResults.cpp +2 -1
@@ 37,7 37,8 @@ namespace gui
                                 msgThreadStyle::ListPositionY,
                                 msgThreadStyle::listWidth,
                                 msgThreadStyle::listHeight,
                                 model);
                                 model,
                                 style::listview::ScrollBarType::Fixed);
        list->setScrollTopMargin(style::margins::small);
        setFocusItem(list);


M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp => module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp +2 -1
@@ 59,7 59,8 @@ namespace gui
                                      musicPlayerStyle::allSongsWindow::y,
                                      musicPlayerStyle::allSongsWindow::w,
                                      musicPlayerStyle::allSongsWindow::h,
                                      songsModel);
                                      songsModel,
                                      style::listview::ScrollBarType::Fixed);

        auto successCallback = [this](const audio::Volume &volume) {
            auto volumeText = audio::GetVolumeText(volume);

M module-apps/application-notes/windows/NoteMainWindow.cpp => module-apps/application-notes/windows/NoteMainWindow.cpp +7 -2
@@ 82,8 82,13 @@ namespace app::notes
                                     windowStyle::search_image::ImageSource);

        namespace listStyle = app::notes::style::list;
        list                = new gui::ListView(
            this, listStyle::X, listStyle::Y, listStyle::Width, listStyle::Height, presenter->getNotesItemProvider());
        list                = new gui::ListView(this,
                                 listStyle::X,
                                 listStyle::Y,
                                 listStyle::Width,
                                 listStyle::Height,
                                 presenter->getNotesItemProvider(),
                                 ::style::listview::ScrollBarType::Fixed);
        list->setPenWidth(listStyle::PenWidth);
        list->setPenFocusWidth(listStyle::FocusedPenWidth);
        list->focusChangedCallback = [this]([[maybe_unused]] gui::Item &item) {

M module-apps/application-notes/windows/SearchResultsWindow.cpp => module-apps/application-notes/windows/SearchResultsWindow.cpp +7 -2
@@ 34,8 34,13 @@ namespace app::notes
        bottomBar->setActive(gui::BottomBar::Side::RIGHT, true);
        bottomBar->setText(gui::BottomBar::Side::RIGHT, utils::localize.get(::style::strings::common::back));

        list =
            new gui::ListView(this, style::list::X, style::list::Y, style::list::Width, style::list::Height, listModel);
        list = new gui::ListView(this,
                                 style::list::X,
                                 style::list::Y,
                                 style::list::Width,
                                 style::list::Height,
                                 listModel,
                                 ::style::listview::ScrollBarType::Fixed);
        list->setScrollTopMargin(::style::margins::small);
        setFocusItem(list);
    }

M module-apps/application-phonebook/windows/PhonebookContactDetails.cpp => module-apps/application-phonebook/windows/PhonebookContactDetails.cpp +2 -1
@@ 36,7 36,8 @@ namespace gui
                                     phonebookStyle::contactDetailsWindow::contactDetailsList::y,
                                     phonebookStyle::contactDetailsWindow::contactDetailsList::w,
                                     phonebookStyle::contactDetailsWindow::contactDetailsList::h,
                                     contactDetailsModel);
                                     contactDetailsModel,
                                     style::listview::ScrollBarType::PreRendered);
        setFocusItem(bodyList);
    }


M module-apps/application-phonebook/windows/PhonebookIceContacts.cpp => module-apps/application-phonebook/windows/PhonebookIceContacts.cpp +2 -1
@@ 32,7 32,8 @@ namespace gui
                                            phonebookStyle::iceContactsWindow::contactsListIce::y,
                                            phonebookStyle::iceContactsWindow::contactsListIce::w,
                                            phonebookStyle::iceContactsWindow::contactsListIce::h,
                                            phonebookModel);
                                            phonebookModel,
                                            style::listview::ScrollBarType::Fixed);

        setFocusItem(contactsListIce);


M module-apps/application-phonebook/windows/PhonebookNewContact.cpp => module-apps/application-phonebook/windows/PhonebookNewContact.cpp +2 -1
@@ 40,7 40,8 @@ namespace gui
                                 phonebookStyle::newContactWindow::newContactsList::y,
                                 phonebookStyle::newContactWindow::newContactsList::w,
                                 phonebookStyle::newContactWindow::newContactsList::h,
                                 newContactModel);
                                 newContactModel,
                                 style::listview::ScrollBarType::PreRendered);
        setFocusItem(list);
    }


M module-gui/gui/widgets/BoxLayout.cpp => module-gui/gui/widgets/BoxLayout.cpp +5 -0
@@ 100,6 100,11 @@ namespace gui
        Item::erase();
    }

    bool BoxLayout::empty() const noexcept
    {
        return children.empty();
    }

    void BoxLayout::setVisible(bool value, bool previous)
    {
        visible = value; // maybe use parent setVisible(...)? would be better but which one?

M module-gui/gui/widgets/BoxLayout.hpp => module-gui/gui/widgets/BoxLayout.hpp +1 -0
@@ 97,6 97,7 @@ namespace gui
        bool removeWidget(Item *item) override;
        bool erase(Item *item) override;
        void erase() override;
        [[nodiscard]] bool empty() const noexcept;
        /// add item if it will fit in box, return true on success
        /// axis sets direction to define space left in container
        template <Axis axis> void addWidget(Item *item);

M module-gui/gui/widgets/ListItemProvider.hpp => module-gui/gui/widgets/ListItemProvider.hpp +2 -2
@@ 26,11 26,11 @@ namespace gui

        virtual unsigned int requestRecordsCount() = 0;

        virtual unsigned int getMinimalItemHeight() const = 0;
        [[nodiscard]] virtual unsigned int getMinimalItemHeight() const = 0;

        virtual ListItem *getItem(Order order) = 0;

        virtual void requestRecords(const uint32_t offset, const uint32_t limit) = 0;
        virtual void requestRecords(uint32_t offset, uint32_t limit) = 0;
    };

} // namespace gui

M module-gui/gui/widgets/ListView.cpp => module-gui/gui/widgets/ListView.cpp +228 -45
@@ 1,4 1,4 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ListView.hpp"


@@ 9,8 9,13 @@
namespace gui
{

    ListViewScroll::ListViewScroll(Item *parent, uint32_t x, uint32_t y, uint32_t w, uint32_t h)
        : Rect{parent, x, y, w, h}
    ListViewScroll::ListViewScroll(Item *parent,
                                   unsigned int x,
                                   unsigned int y,
                                   unsigned int w,
                                   unsigned int h,
                                   style::listview::ScrollBarType type)
        : Rect{parent, x, y, w, h}, type(type)
    {

        setRadius(style::listview::scroll::radius);


@@ 19,29 24,97 @@ namespace gui
        activeItem = false;
    }

    bool ListViewScroll::shouldShowScroll(unsigned int currentPageSize, unsigned int elementsCount)
    void ListViewScroll::updateProportional(const ListViewScrollUpdateData &data)
    {
        double scrollStep =
            static_cast<double>((parent->widgetArea.h - data.topMargin)) / static_cast<double>(data.elementsCount);

        auto scrollH = scrollStep * data.listPageSize;
        auto scrollY = scrollStep * data.startIndex > 0 ? scrollStep * data.startIndex : data.topMargin;

        setArea(BoundingBox(
            parent->widgetArea.w - style::listview::scroll::margin, scrollY, style::listview::scroll::w, scrollH));
    }

    void ListViewScroll::updateFixed(const ListViewScrollUpdateData &data)
    {
        auto elementsOnPage = (parent->widgetArea.h - data.topMargin) / data.elementMinimalHeight;

        pagesCount = data.elementsCount % elementsOnPage == 0 ? data.elementsCount / elementsOnPage
                                                              : data.elementsCount / elementsOnPage + 1;

        currentPage = data.startIndex / elementsOnPage;

        auto scrollH = (parent->widgetArea.h - data.topMargin) / pagesCount;
        auto scrollY = scrollH * currentPage > 0 ? scrollH * currentPage : data.topMargin;

        setArea(BoundingBox(
            parent->widgetArea.w - style::listview::scroll::margin, scrollY, style::listview::scroll::w, scrollH));
    }

    void ListViewScroll::updatePreRendered(const ListViewScrollUpdateData &data)
    {
        if (data.startIndex != storedStartIndex) {
            if (data.direction == style::listview::Direction::Bottom) {
                if (data.boundaries == style::listview::Boundaries::Continuous && (data.startIndex == 0)) {
                    currentPage = 0;
                }
                else if (currentPage + 1 < pagesCount) {
                    currentPage++;
                }
            }
            else {
                if (data.boundaries == style::listview::Boundaries::Continuous && storedStartIndex == 0) {
                    currentPage = pagesCount - 1;
                }
                else if (currentPage > 0 && storedStartIndex != 0) {
                    currentPage--;
                }
            }
        }

        storedStartIndex = data.startIndex;

        auto scrollH = (parent->widgetArea.h - data.topMargin) / pagesCount;
        auto scrollY = currentPage * scrollH > 0 ? currentPage * scrollH : data.topMargin;

        setArea(BoundingBox(
            parent->widgetArea.w - style::listview::scroll::margin, scrollY, style::listview::scroll::w, scrollH));
    }

    void ListViewScroll::updateStartConditions(const unsigned int index,
                                               const unsigned int page,
                                               const unsigned int count)
    {
        storedStartIndex = index;
        currentPage      = page;
        pagesCount       = count;
    }

    bool ListViewScroll::shouldShowScroll(unsigned int currentPageSize, unsigned int elementsCount)
    {
        return ((parent->widgetArea.w > style::listview::scroll::min_space) &&
                (parent->widgetArea.h > style::listview::scroll::min_space) && currentPageSize < elementsCount);
    }

    void ListViewScroll::update(unsigned int startIndex,
                                unsigned int currentPageSize,
                                unsigned int elementsCount,
                                int topMargin)
    void ListViewScroll::update(const ListViewScrollUpdateData &data)
    {
        if (shouldShowScroll(currentPageSize, elementsCount)) {
        if (shouldShowScroll(data.listPageSize, data.elementsCount)) {

            assert(elementsCount != 0);
            double scrollStep =
                static_cast<double>((parent->widgetArea.h - topMargin)) / static_cast<double>(elementsCount);

            auto scrollH = scrollStep * currentPageSize;
            auto scrollY = scrollStep * startIndex > 0 ? scrollStep * startIndex : topMargin;
            switch (type) {
            case style::listview::ScrollBarType::Proportional:
                updateProportional(data);
                break;
            case style::listview::ScrollBarType::Fixed:
                updateFixed(data);
                break;
            case style::listview::ScrollBarType::PreRendered:
                updatePreRendered(data);
                break;
            case style::listview::ScrollBarType::None:
                break;
            }

            setArea(BoundingBox(
                parent->widgetArea.w - style::listview::scroll::margin, scrollY, style::listview::scroll::w, scrollH));
            setVisible(true);
        }
        else


@@ 56,15 129,16 @@ namespace gui
                                    style::listview::scroll::x,
                                    style::listview::scroll::y,
                                    style::listview::scroll::w,
                                    style::listview::scroll::h);
                                    style::listview::scroll::h,
                                    style::listview::ScrollBarType::None);
        type   = gui::ItemType::LIST;
    }

    ListView::ListView(Item *parent,
                       uint32_t x,
                       uint32_t y,
                       uint32_t w,
                       uint32_t h,
                       unsigned int x,
                       unsigned int y,
                       unsigned int w,
                       unsigned int h,
                       std::shared_ptr<ListItemProvider> prov,
                       style::listview::ScrollBarType scrollBarType)
        : Rect{parent, x, y, w, h}


@@ 101,7 175,8 @@ namespace gui
                                        style::listview::scroll::x,
                                        style::listview::scroll::y,
                                        style::listview::scroll::w,
                                        style::listview::scroll::h);
                                        style::listview::scroll::h,
                                        scrollBarType);
        }

        setProvider(std::move(prov));


@@ 116,7 191,10 @@ namespace gui

    void ListView::setElementsCount(unsigned int count)
    {
        elementsCount = count;
        if (elementsCount != count) {
            elementsCount = count;
            onElementsCountChanged();
        }
    }

    void ListView::setBoundaries(style::listview::Boundaries value)


@@ 189,24 267,20 @@ namespace gui
    {
        if (rebuildType == style::listview::RebuildType::Full) {
            setStartIndex();
            storedFocusIndex = 0;
            storedFocusIndex = style::listview::nPos;
        }
        else if (rebuildType == style::listview::RebuildType::OnOffset) {
            if (dataOffset < elementsCount) {
                startIndex       = dataOffset;
                storedFocusIndex = 0;
                storedFocusIndex = style::listview::nPos;
            }
            else {
                LOG_ERROR("Requested rebuild on index greater than elements count");
            }
        }
        else if (rebuildType == style::listview::RebuildType::InPlace) {

            storedFocusIndex = body->getFocusItemIndex();

            if (direction == style::listview::Direction::Top) {
                int position     = currentPageSize - 1 - storedFocusIndex;
                storedFocusIndex = std::abs(position);
            if (!body->empty()) {
                storedFocusIndex = getFocusItemIndex();
            }
        }



@@ 216,6 290,18 @@ namespace gui
        direction = style::listview::Direction::Bottom;
    }

    unsigned int ListView::getFocusItemIndex()
    {
        auto index = body->getFocusItemIndex();

        if (direction == style::listview::Direction::Top) {
            int position = currentPageSize - 1 - index;
            index        = std::abs(position);
        }

        return index;
    }

    std::shared_ptr<ListItemProvider> ListView::getProvider()
    {
        return provider;


@@ 263,15 349,19 @@ namespace gui
            return;
        }

        onElementsCountChanged();

        clearItems();

        addItemsOnPage();

        setFocus();
        if (scroll) {
            scroll->update(startIndex, currentPageSize, elementsCount, scrollTopMargin);
            scroll->update(ListViewScrollUpdateData{startIndex,
                                                    currentPageSize,
                                                    elementsCount,
                                                    provider->getMinimalItemHeight(),
                                                    direction,
                                                    boundaries,
                                                    scrollTopMargin});
        }
        resizeWithScroll();
        pageLoaded = true;


@@ 287,10 377,14 @@ namespace gui

    void ListView::onProviderDataUpdate()
    {
        if (!renderFullList()) {
            return;
        }

        refresh();
    }

    Order ListView::getOrderFromDirection()
    Order ListView::getOrderFromDirection() const noexcept
    {
        if (direction == style::listview::Direction::Bottom)
            return Order::Next;


@@ 298,6 392,14 @@ namespace gui
        return Order::Previous;
    }

    Order ListView::getOppositeOrderFromDirection() const noexcept
    {
        if (direction == style::listview::Direction::Bottom)
            return Order::Previous;

        return Order::Next;
    }

    void ListView::setStartIndex()
    {
        if (orientation == style::listview::Orientation::TopBottom) {


@@ 311,7 413,6 @@ namespace gui
    void ListView::recalculateStartIndex()
    {
        if (direction == style::listview::Direction::Top) {

            startIndex = startIndex < currentPageSize ? 0 : startIndex - currentPageSize;
        }
    }


@@ 324,6 425,8 @@ namespace gui
            startIndex == 0) {
            if (body->getSizeLeft() > provider->getMinimalItemHeight()) {
                focusOnLastItem = true;

                checkFullRenderRequirement();
                rebuildList();
            }
        }


@@ 332,6 435,8 @@ namespace gui
            startIndex + currentPageSize == elementsCount) {
            if (body->getSizeLeft() > provider->getMinimalItemHeight()) {
                focusOnLastItem = true;

                checkFullRenderRequirement();
                rebuildList();
            }
        }


@@ 357,7 462,11 @@ namespace gui

            body->addWidget(item);

            if (item->visible != true) {
            if (!item->visible) {
                // In case model is tracking internal indexes -> undo last get.
                if (requestFullListRender) {
                    provider->getItem(getOppositeOrderFromDirection());
                }
                break;
            }



@@ 367,11 476,75 @@ namespace gui
        recalculateStartIndex();
    }

    void ListView::checkFullRenderRequirement()
    {
        if (scroll && scroll->type == style::listview::ScrollBarType::PreRendered) {
            requestFullListRender = true;
        }
    }

    bool ListView::renderFullList()
    {
        if (!requestFullListRender) {
            return true;
        }

        if (elementsCount != 0 && !requestCompleteData) {
            requestCompleteData = true;
            provider->requestRecords(0, elementsCount);
            return false;
        }

        if (requestCompleteData) {

            auto page           = 0;
            auto pageStartIndex = 0;

            clearItems();

            while (true) {

                addItemsOnPage();

                if (currentPageSize == 0) {
                    break;
                }

                if (currentPageSize + pageStartIndex == elementsCount) {
                    break;
                }

                page += 1;
                pageStartIndex += currentPageSize;

                clearItems();
            }

            clearItems();
            requestCompleteData   = false;
            requestFullListRender = false;

            if (lastRebuildRequest.first == style::listview::RebuildType::Full) {
                if (orientation == style::listview::Orientation::TopBottom) {
                    scroll->updateStartConditions(startIndex, 0, page + 1);
                }
                else {
                    scroll->updateStartConditions(startIndex, page, page + 1);
                }
            }

            reSendLastRebuildRequest();
            return false;
        }

        return true;
    } // namespace gui

    void ListView::setFocus()
    {
        setFocusItem(body);

        if (storedFocusIndex != 0) {
        if (storedFocusIndex != style::listview::nPos) {
            if (!body->setFocusOnElement(storedFocusIndex)) {
                body->setFocusOnLastElement();
            }


@@ 393,6 566,8 @@ namespace gui
        else if (notEmptyListCallback) {
            notEmptyListCallback();
        }

        checkFullRenderRequirement();
    }

    bool ListView::onDimensionChanged(const BoundingBox &oldDim, const BoundingBox &newDim)


@@ 400,7 575,13 @@ namespace gui
        Rect::onDimensionChanged(oldDim, newDim);
        body->setSize(body->getWidth(), newDim.h);
        if (scroll) {
            scroll->update(startIndex, currentPageSize, elementsCount, scrollTopMargin);
            scroll->update(ListViewScrollUpdateData{startIndex,
                                                    currentPageSize,
                                                    elementsCount,
                                                    provider->getMinimalItemHeight(),
                                                    direction,
                                                    boundaries,
                                                    scrollTopMargin});
        }

        return true;


@@ 430,7 611,8 @@ namespace gui
                startIndex = startIndex < diff ? 0 : startIndex - diff;
            }

            fillFirstPage();
            checkFullRenderRequirement();
            rebuildList();
        }
    }



@@ 476,7 658,7 @@ namespace gui
        direction = style::listview::Direction::Bottom;
        body->setReverseOrder(false);
        pageLoaded       = false;
        storedFocusIndex = 0;
        storedFocusIndex = style::listview::nPos;
        provider->requestRecords(startIndex, calculateLimit());

        return true;


@@ 489,9 671,9 @@ namespace gui

        if (startIndex == 0 && boundaries == style::listview::Boundaries::Continuous) {

            topFetchIndex = elementsCount - (elementsCount % currentPageSize);
            startIndex    = elementsCount;
            limit         = calculateLimit(style::listview::Direction::Top) - topFetchIndex;
            topFetchIndex = elementsCount - calculateLimit(style::listview::Direction::Top);
            limit         = calculateLimit(style::listview::Direction::Top);
        }
        else if (startIndex == 0 && boundaries == style::listview::Boundaries::Fixed) {



@@ 508,9 690,10 @@ namespace gui
        direction = style::listview::Direction::Top;
        body->setReverseOrder(true);
        pageLoaded       = false;
        storedFocusIndex = 0;
        storedFocusIndex = style::listview::nPos;
        provider->requestRecords(topFetchIndex, limit);

        return true;
    }

} /* namespace gui */

M module-gui/gui/widgets/ListView.hpp => module-gui/gui/widgets/ListView.hpp +46 -8
@@ 16,20 16,50 @@ namespace gui

    using rebuildRequest = std::pair<style::listview::RebuildType, unsigned int>;

    struct ListViewScrollUpdateData
    {
        const unsigned int startIndex;
        const unsigned int listPageSize;
        const unsigned int elementsCount;
        const unsigned int elementMinimalHeight;
        const style::listview::Direction direction;
        const style::listview::Boundaries boundaries;
        const int topMargin;
    };

    class ListViewScroll : public Rect
    {
      private:
        unsigned int storedStartIndex = 0;
        unsigned int currentPage      = style::listview::nPos;
        unsigned int pagesCount       = 0;

        void updateProportional(const ListViewScrollUpdateData &data);
        void updateFixed(const ListViewScrollUpdateData &data);
        void updatePreRendered(const ListViewScrollUpdateData &data);

      public:
        ListViewScroll(Item *parent, uint32_t x, uint32_t y, uint32_t w, uint32_t h);
        style::listview::ScrollBarType type = style::listview::ScrollBarType::None;

        ListViewScroll(Item *parent,
                       unsigned int x,
                       unsigned int y,
                       unsigned int w,
                       unsigned int h,
                       style::listview::ScrollBarType type);

        bool shouldShowScroll(unsigned int listPageSize, unsigned int elementsCount);
        void update(unsigned int startIndex, unsigned int listPageSize, unsigned int elementsCount, int topMargin);
        void updateStartConditions(const unsigned int storedStartIndex,
                                   const unsigned int currentPage,
                                   const unsigned int pagesCount);
        void update(const ListViewScrollUpdateData &data);
    };

    class ListView : public Rect
    {
      protected:
        unsigned int startIndex                    = 0;
        unsigned int storedFocusIndex              = 0;
        unsigned int storedFocusIndex              = style::listview::nPos;
        unsigned int elementsCount                 = 0;
        std::shared_ptr<ListItemProvider> provider = nullptr;
        VBox *body                                 = nullptr;


@@ 48,6 78,12 @@ namespace gui

        void clearItems();
        virtual void addItemsOnPage();

        bool requestCompleteData   = false;
        bool requestFullListRender = false;
        bool renderFullList();
        void checkFullRenderRequirement();

        void setFocus();
        void refresh();
        void resizeWithScroll();


@@ 55,11 91,13 @@ namespace gui
        void fillFirstPage();
        void setStartIndex();
        void recalculateOnBoxRequestedResize();
        [[nodiscard]] unsigned int getFocusItemIndex();
        /// Default empty list to inform that there is no elements - callback should be override in applications
        void onElementsCountChanged();
        unsigned int calculateMaxItemsOnPage();
        unsigned int calculateLimit(style::listview::Direction value = style::listview::Direction::Bottom);
        Order getOrderFromDirection();
        [[nodiscard]] Order getOrderFromDirection() const noexcept;
        [[nodiscard]] Order getOppositeOrderFromDirection() const noexcept;
        virtual bool requestNextPage();
        virtual bool requestPreviousPage();
        void setup(style::listview::RebuildType rebuildType, unsigned int dataOffset = 0);


@@ 67,10 105,10 @@ namespace gui
      public:
        ListView();
        ListView(Item *parent,
                 uint32_t x,
                 uint32_t y,
                 uint32_t w,
                 uint32_t h,
                 unsigned int x,
                 unsigned int y,
                 unsigned int w,
                 unsigned int h,
                 std::shared_ptr<ListItemProvider> prov,
                 style::listview::ScrollBarType scrollType = style::listview::ScrollBarType::Proportional);
        ~ListView();

M module-gui/gui/widgets/Style.hpp => module-gui/gui/widgets/Style.hpp +9 -1
@@ 3,6 3,7 @@

#pragma once

#include <limits>
#include <gui/core/Color.hpp>
#include <gui/Common.hpp>
#include <Alignment.hpp>


@@ 178,6 179,8 @@ namespace style

    namespace listview
    {
        inline constexpr auto nPos = std::numeric_limits<unsigned int>::max();

        /// Possible List boundaries handling types
        enum class Boundaries
        {


@@ 208,7 211,12 @@ namespace style
        {
            None,         ///< None - list without scroll bar (but with scrolling).
            Proportional, ///< Proportional - scroll bar size calculated based on elements count in model and currently
                          ///< displayed number of elements.
                          ///< displayed number of elements. Use with large unequal heights lists elements.
            Fixed,        ///< Fixed - scroll bar size calculated based on fixed equal elements sizes in list.
                          ///< Use when all elements have equal heights.
            PreRendered   ///< PreRendered - scroll bar size calculated based on pre rendered pages on whole list. Use
                          ///< when elements are not equal heights but there are few of them as its renders whole
                          ///< context and can be time consuming.
        };

        enum class Orientation

M module-gui/test/test-google/test-gui-listview.cpp => module-gui/test/test-google/test-gui-listview.cpp +7 -7
@@ 239,11 239,11 @@ TEST_F(ListViewTesting, Continuous_Type_Test)
    moveNTimes(1, style::listview::Direction::Top);
    ASSERT_TRUE(testListView->listBorderReached) << "Navigate top by one - page should change to last page";
    testListView->listBorderReached = false;
    ASSERT_EQ(4, testListView->currentPageSize) << "4 elements should be displayed";
    ASSERT_EQ(6, testListView->currentPageSize) << "6 elements should be displayed";

    ASSERT_EQ(9, dynamic_cast<gui::TestListItem *>(testListView->body->children.front())->ID)
        << "First element ID should be 9";
    ASSERT_EQ(6, dynamic_cast<gui::TestListItem *>(testListView->body->children.back())->ID)
    ASSERT_EQ(3, dynamic_cast<gui::TestListItem *>(testListView->body->children.back())->ID)
        << "Last element ID should be 3 (9 - 6)";
    ASSERT_EQ(style::listview::Direction::Top, testListView->direction) << "List Direction should be Top";



@@ 251,11 251,11 @@ TEST_F(ListViewTesting, Continuous_Type_Test)
    ASSERT_TRUE(testListView->listBorderReached) << "Navigate top by page size - page should change";
    testListView->listBorderReached = false;
    ASSERT_EQ(6, testListView->currentPageSize) << "6 elements should be displayed";
    ASSERT_EQ(5, dynamic_cast<gui::TestListItem *>(testListView->body->children.front())->ID)
        << "First element ID should be 5";
    ASSERT_EQ(0, dynamic_cast<gui::TestListItem *>(testListView->body->children.back())->ID)
        << "Last element ID should be 0";
    ASSERT_EQ(style::listview::Direction::Top, testListView->direction) << "List Direction should be Top";
    ASSERT_EQ(0, dynamic_cast<gui::TestListItem *>(testListView->body->children.front())->ID)
        << "First element ID should be 0";
    ASSERT_EQ(6, dynamic_cast<gui::TestListItem *>(testListView->body->children.back())->ID)
        << "Last element ID should be 6";
    ASSERT_EQ(style::listview::Direction::Bottom, testListView->direction) << "List Direction should be Bottom";

    moveNTimes(1, style::listview::Direction::Bottom);
    ASSERT_TRUE(testListView->listBorderReached) << "Navigate bot by one - page should change";