From 4fe67b36e3bbe6ea5505ed7233c245be9374f0ce Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Sun, 2 Feb 2025 20:59:36 +0100 Subject: refactor selection --- lib/nes/app/ui/CMakeLists.txt | 5 +- lib/nes/app/ui/action.hh | 6 +- lib/nes/app/ui/screen-browser.cc | 71 ++++++++++++++++++++++ lib/nes/app/ui/screen-browser.hh | 32 ++++++++++ lib/nes/app/ui/screen-title.cc | 61 +++++++++++++------ lib/nes/app/ui/screen-title.hh | 19 ++++-- lib/nes/app/ui/selection.hh | 125 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 294 insertions(+), 25 deletions(-) create mode 100644 lib/nes/app/ui/screen-browser.cc create mode 100644 lib/nes/app/ui/screen-browser.hh create mode 100644 lib/nes/app/ui/selection.hh (limited to 'lib/nes/app/ui') diff --git a/lib/nes/app/ui/CMakeLists.txt b/lib/nes/app/ui/CMakeLists.txt index 2faed29..0980e97 100644 --- a/lib/nes/app/ui/CMakeLists.txt +++ b/lib/nes/app/ui/CMakeLists.txt @@ -2,6 +2,9 @@ target_sources( nes PRIVATE action.hh + selection.hh screen.hh screen-title.hh - screen-title.cc) \ No newline at end of file + screen-title.cc + screen-browser.hh + screen-browser.cc) \ No newline at end of file diff --git a/lib/nes/app/ui/action.hh b/lib/nes/app/ui/action.hh index 2994f37..52e5ba6 100644 --- a/lib/nes/app/ui/action.hh +++ b/lib/nes/app/ui/action.hh @@ -6,6 +6,10 @@ namespace nes::app enum class action { none, - load_game, + go_to_title, + go_to_browser, + go_to_settings, + go_to_help, + launch_game, }; } // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/ui/screen-browser.cc b/lib/nes/app/ui/screen-browser.cc new file mode 100644 index 0000000..da2d82d --- /dev/null +++ b/lib/nes/app/ui/screen-browser.cc @@ -0,0 +1,71 @@ +#include "nes/app/ui/screen-browser.hh" +#include "nes/app/graphics/renderer.hh" +#include "nes/app/graphics/color.hh" +#include "nes/app/input/input-device-keyboard.hh" +#include "nes/common/string-builder.hh" + +namespace nes::app +{ + auto screen_browser::selection_impl::render_item( + renderer& renderer, i32 const& item, i32 const x, i32 const y, u32 const width, color const color) const -> void + { + renderer.render_text_format(x, y, color, text_attributes{}.set_max_width(width), "Item {}", item); + } + + auto screen_browser::selection_impl::load_page(int (&items)[5], u32 const page) -> u32 + { + for (auto i = u32{ 0 }; i < 5; ++i) + { + items[i] = static_cast(page * 5 + i); + } + return 5; + } + + screen_browser::screen_browser(input_device_keyboard& keyboard) + : keyboard_{ keyboard } + { + selection_.set_page_count(2); + } + + auto screen_browser::render(renderer& renderer) -> void + { + renderer.render_fill(color::background_primary); + renderer.render_rect(0, 0, 32, 3, color::background_secondary); + + renderer.render_text(2, 1, "Load Game", color::foreground_primary); + renderer.render_text_format( + 30, 1, color::foreground_secondary, text_attributes{}.set_alignment(text_alignment::right), + "Page {}/{}", selection_.get_current_page() + 1, selection_.get_page_count()); + + selection_.render(renderer); + } + + auto screen_browser::process_events() -> action + { + while (auto const event = keyboard_.poll_event()) + { + if (event == input_event::key_down(key::space)) + { + selection_.go_next(); + } + else if (event == input_event::key_down(key::arrow_up)) + { + selection_.go_up(); + } + else if (event == input_event::key_down(key::arrow_down)) + { + selection_.go_down(); + } + else if (event == input_event::key_down(key::backspace)) + { + return action::go_to_title; + } + else if (event == input_event::key_down(key::enter)) + { + return action::launch_game; + } + } + + return action::none; + } +} // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/ui/screen-browser.hh b/lib/nes/app/ui/screen-browser.hh new file mode 100644 index 0000000..eb66266 --- /dev/null +++ b/lib/nes/app/ui/screen-browser.hh @@ -0,0 +1,32 @@ +#pragma once + +#include "nes/app/ui/screen.hh" +#include "nes/app/ui/selection.hh" + +namespace nes::app +{ + class input_device_keyboard; + + /// An interface which allows the user to browse the filesystem for a game. + class screen_browser final : public screen + { + class selection_impl final : public selection + { + public: + using selection::selection; + + protected: + auto render_item(renderer&, i32 const&, i32 x, i32 y, u32 width, color) const -> void override; + auto load_page(i32(&)[5], u32 page) -> u32 override; + }; + + input_device_keyboard& keyboard_; + selection_impl selection_{ 2, 5, 28 }; + + public: + explicit screen_browser(input_device_keyboard&); + + auto render(renderer&) -> void override; + auto process_events() -> action override; + }; +} // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/ui/screen-title.cc b/lib/nes/app/ui/screen-title.cc index 5a2f757..32a6ea6 100644 --- a/lib/nes/app/ui/screen-title.cc +++ b/lib/nes/app/ui/screen-title.cc @@ -2,13 +2,35 @@ #include "nes/app/graphics/renderer.hh" #include "nes/app/graphics/color.hh" #include "nes/app/graphics/tiles/banner.hh" -#include "nes/app/input/input-manager.hh" +#include "nes/app/input/input-device-keyboard.hh" namespace nes::app { - screen_title::screen_title(input_manager& input_manager) - : input_manager_{ input_manager } + auto screen_title::selection_impl::render_item( + renderer& renderer, item const& item, i32 const x, i32 const y, u32 const width, color const color) const -> void { + auto const attrs = text_attributes{} + .set_max_width(width); + switch (item) + { + case item::load_game: renderer.render_text(x, y, "Load Game", color, attrs); break; + case item::settings: renderer.render_text(x, y, "Settings", color, attrs); break; + case item::help: renderer.render_text(x, y, "Help", color, attrs); break; + } + } + + auto screen_title::selection_impl::load_page(item (&items)[3], u32) -> u32 + { + items[0] = item::load_game; + items[1] = item::settings; + items[2] = item::help; + return 3; + } + + screen_title::screen_title(input_device_keyboard& keyboard) + : keyboard_{ keyboard } + { + selection_.set_page_count(1); } auto screen_title::render(renderer& renderer) -> void @@ -16,32 +38,33 @@ namespace nes::app renderer.render_fill(color::background_primary); renderer.render_image(2, 4, tiles::banner); - if (selected_item_ == item::load_game) { renderer.render_text(10, 16, ">", color::accent_primary); } - renderer.render_text(12, 16, "Load Game", selected_item_ == item::load_game ? color::accent_primary : color::foreground_primary); - - if (selected_item_ == item::settings) { renderer.render_text(10, 19, ">", color::accent_primary); } - renderer.render_text(12, 19, "Settings", selected_item_ == item::settings ? color::accent_primary : color::foreground_primary); - - if (selected_item_ == item::help) { renderer.render_text(10, 22, ">", color::accent_primary); } - renderer.render_text(12, 22, "Help", selected_item_ == item::help ? color::accent_primary : color::foreground_primary); + selection_.render(renderer); } auto screen_title::process_events() -> action { - while (auto event = input_manager_.get_keyboard().poll_event()) + while (auto const event = keyboard_.poll_event()) { if (event == input_event::key_down(key::space)) { - switch (selected_item_) - { - case item::load_game: selected_item_ = item::settings; break; - case item::settings: selected_item_ = item::help; break; - case item::help: selected_item_ = item::load_game; break; - } + selection_.go_next(); + } + else if (event == input_event::key_down(key::arrow_up)) + { + selection_.go_up(); + } + else if (event == input_event::key_down(key::arrow_down)) + { + selection_.go_down(); } else if (event == input_event::key_down(key::enter)) { - return action::load_game; + switch (*selection_.get_selected()) + { + case item::load_game: return action::go_to_browser; + case item::settings: return action::go_to_settings; + case item::help: return action::go_to_help; + } } } diff --git a/lib/nes/app/ui/screen-title.hh b/lib/nes/app/ui/screen-title.hh index 2ebc78a..6384c3e 100644 --- a/lib/nes/app/ui/screen-title.hh +++ b/lib/nes/app/ui/screen-title.hh @@ -1,10 +1,11 @@ #pragma once #include "nes/app/ui/screen.hh" +#include "nes/app/ui/selection.hh" namespace nes::app { - class input_manager; + class input_device_keyboard; /// The main title screen for the emulator, used as an entrypoint for the UI. /// @@ -18,11 +19,21 @@ namespace nes::app help, }; - input_manager& input_manager_; - item selected_item_{ item::load_game }; + class selection_impl final : public selection + { + public: + using selection::selection; + + protected: + auto render_item(renderer&, item const&, i32 x, i32 y, u32 width, color) const -> void override; + auto load_page(item(&)[3], u32 page) -> u32 override; + }; + + input_device_keyboard& keyboard_; + selection_impl selection_{ 10, 16, 11 }; public: - explicit screen_title(input_manager&); + explicit screen_title(input_device_keyboard&); auto render(renderer&) -> void override; auto process_events() -> action override; diff --git a/lib/nes/app/ui/selection.hh b/lib/nes/app/ui/selection.hh new file mode 100644 index 0000000..fafa4a6 --- /dev/null +++ b/lib/nes/app/ui/selection.hh @@ -0,0 +1,125 @@ +#pragma once + +#include "nes/common/types.hh" +#include "nes/app/graphics/renderer.hh" +#include "nes/app/graphics/color.hh" + +namespace nes::app +{ + /// A selection menu managing a fixed number of items and pagination. + template + class selection + { + Item items_[PageSize]; + u32 count_{ 0 }; + u32 page_count_{ 0 }; + i32 x_{}; + i32 y_{}; + u32 width_{}; + i32 selected_{ -1 }; + i32 current_page_{ -1 }; + + public: + explicit selection(i32 const x, i32 const y, u32 const width) + : x_{ x } + , y_{ y } + , width_{ width } + { + } + + virtual ~selection() = default; + + selection(selection const&) = delete; + selection(selection&&) = delete; + auto operator=(selection const&) -> selection& = delete; + auto operator=(selection&&) -> selection& = delete; + + auto set_page_count(u32 const page_count) -> void + { + page_count_ = page_count; + set_page(page_count_ > 0 ? 0 : -1); + } + + auto get_page_count() const -> u32 { return page_count_; } + auto get_current_page() const -> i32 { return current_page_; } + auto get_selected() const -> Item const* { return selected_ >= 0 ? &items_[selected_] : nullptr; } + + auto render(renderer& renderer) const -> void + { + for (auto i = u32{ 0 }; i < count_; ++i) + { + auto const y = y_ + static_cast(3 * i); + auto const is_selected = static_cast(i) == selected_; + auto const color = is_selected ? color::accent_primary : color::foreground_primary; + if (is_selected) + { + renderer.render_text(x_, y, ">", color); + } + + render_item(renderer, items_[i], x_ + 2, y, width_ - 2, color); + } + } + + auto go_next() -> void + { + if (selected_ < 0) { return; } + + selected_ += 1; + if (selected_ == static_cast(count_)) { selected_ = 0; } + } + + auto go_up() -> void + { + if (selected_ <= 0) + { + if (current_page_ <= 0) { return; } + + set_page(current_page_ - 1, true); + return; + } + selected_ -= 1; + } + + auto go_down() -> void + { + if (selected_ < 0 || selected_ + 1 == static_cast(count_)) + { + if (current_page_ < 0 || current_page_ + 1 == static_cast(page_count_)) { return; } + + set_page(current_page_ + 1, false); + return; + } + selected_ += 1; + } + + protected: + /// Render an item's caption at the given position and width. + virtual auto render_item(renderer&, Item const&, i32 x, i32 y, u32 width, color) const -> void = 0; + + /// Load the items for a page (max. PageSize items) and return the number of loaded items. + virtual auto load_page(Item(&)[PageSize], u32 page) -> u32 = 0; + + private: + auto set_page(i32 const page, bool const select_last = false) -> void + { + if (page < 0) + { + current_page_ = -1; + count_ = 0; + selected_ = -1; + return; + } + + current_page_ = page; + count_ = load_page(items_, static_cast(page)); + if (count_ > 0) + { + selected_ = select_last ? static_cast(count_) - 1 : 0; + } + else + { + selected_ = -1; + } + } + }; +} // namespace nes::app \ No newline at end of file -- cgit v1.3.1