summaryrefslogtreecommitdiff
path: root/lib/nes/app/ui
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2025-02-02 20:59:36 +0100
committerHannes Schulze <haschu0103@outlook.de>2025-02-02 20:59:36 +0100
commit4fe67b36e3bbe6ea5505ed7233c245be9374f0ce (patch)
treef5cfe88f252ee2e9830162a1673441a704e6ea37 /lib/nes/app/ui
parent8062afc25e895cb49e76f6e26e51a7d4a1ad2df2 (diff)
refactor selection
Diffstat (limited to 'lib/nes/app/ui')
-rw-r--r--lib/nes/app/ui/CMakeLists.txt5
-rw-r--r--lib/nes/app/ui/action.hh6
-rw-r--r--lib/nes/app/ui/screen-browser.cc71
-rw-r--r--lib/nes/app/ui/screen-browser.hh32
-rw-r--r--lib/nes/app/ui/screen-title.cc61
-rw-r--r--lib/nes/app/ui/screen-title.hh19
-rw-r--r--lib/nes/app/ui/selection.hh125
7 files changed, 294 insertions, 25 deletions
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<i32>(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<i32, 5>
+ {
+ 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<item, 3>
+ {
+ 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<typename Item, u32 PageSize>
+ 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<i32>(3 * i);
+ auto const is_selected = static_cast<i32>(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<i32>(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<i32>(count_))
+ {
+ if (current_page_ < 0 || current_page_ + 1 == static_cast<i32>(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<u32>(page));
+ if (count_ > 0)
+ {
+ selected_ = select_last ? static_cast<i32>(count_) - 1 : 0;
+ }
+ else
+ {
+ selected_ = -1;
+ }
+ }
+ };
+} // namespace nes::app \ No newline at end of file