summaryrefslogtreecommitdiff
path: root/lib/nes/app
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
parent8062afc25e895cb49e76f6e26e51a7d4a1ad2df2 (diff)
refactor selection
Diffstat (limited to 'lib/nes/app')
-rw-r--r--lib/nes/app/application.cc17
-rw-r--r--lib/nes/app/application.hh4
-rw-r--r--lib/nes/app/graphics/CMakeLists.txt1
-rw-r--r--lib/nes/app/graphics/renderer.cc95
-rw-r--r--lib/nes/app/graphics/renderer.hh35
-rw-r--r--lib/nes/app/graphics/text-attributes.hh39
-rw-r--r--lib/nes/app/input/input-buffer.hh1
-rw-r--r--lib/nes/app/input/input-event.hh1
-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
15 files changed, 456 insertions, 56 deletions
diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc
index b18e6d6..5f73e01 100644
--- a/lib/nes/app/application.cc
+++ b/lib/nes/app/application.cc
@@ -28,7 +28,8 @@ namespace nes::app
application::application(display& display, input_device_keyboard& keyboard)
: display_{ display }
, input_manager_{ keyboard }
- , screen_title_{ input_manager_ }
+ , screen_title_{ keyboard }
+ , screen_browser_{ keyboard }
{
display_.screen = &screen_title_;
}
@@ -72,7 +73,19 @@ namespace nes::app
{
break;
}
- case action::load_game:
+ case action::go_to_title:
+ {
+ console_.clear();
+ display_.screen = &screen_title_;
+ break;
+ }
+ case action::go_to_browser:
+ {
+ console_.clear();
+ display_.screen = &screen_browser_;
+ break;
+ }
+ case action::launch_game:
{
auto stream = std::ifstream{ file, std::ios::binary };
if (!stream.good())
diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh
index 91ab35f..44b99bb 100644
--- a/lib/nes/app/application.hh
+++ b/lib/nes/app/application.hh
@@ -2,6 +2,7 @@
#include "nes/app/input/input-manager.hh"
#include "nes/app/ui/screen-title.hh"
+#include "nes/app/ui/screen-browser.hh"
#include "nes/sys/nes.hh"
#include "nes/common/box.hh"
#include "nes/common/display.hh"
@@ -40,8 +41,9 @@ namespace nes::app
display_proxy display_;
input_manager input_manager_;
- box<sys::nes> console_;
+ box<sys::nes> console_{};
screen_title screen_title_;
+ screen_browser screen_browser_;
public:
explicit application(display&, input_device_keyboard&);
diff --git a/lib/nes/app/graphics/CMakeLists.txt b/lib/nes/app/graphics/CMakeLists.txt
index 0f869b7..2415f4f 100644
--- a/lib/nes/app/graphics/CMakeLists.txt
+++ b/lib/nes/app/graphics/CMakeLists.txt
@@ -5,6 +5,7 @@ target_sources(
image-view.hh
mask-tile.hh
image-tile.hh
+ text-attributes.hh
renderer.hh
renderer.cc)
diff --git a/lib/nes/app/graphics/renderer.cc b/lib/nes/app/graphics/renderer.cc
index 6a45b6c..6429a52 100644
--- a/lib/nes/app/graphics/renderer.cc
+++ b/lib/nes/app/graphics/renderer.cc
@@ -3,6 +3,7 @@
#include "nes/app/graphics/mask-tile.hh"
#include "nes/app/graphics/image-tile.hh"
#include "nes/app/graphics/image-view.hh"
+#include "nes/app/graphics/text-attributes.hh"
#include "nes/app/graphics/tiles/characters.hh"
#include "nes/common/display.hh"
@@ -13,9 +14,9 @@ namespace nes::app
{
}
- auto renderer::render_image_tile(u32 const x, u32 const y, image_tile const& tile) -> void
+ auto renderer::render_image_tile(i32 const x, i32 const y, image_tile const& tile) -> void
{
- if (x >= width || y >= height) { return; }
+ if (x < 0 || y < 0 || x >= static_cast<i32>(width) || y >= static_cast<i32>(height)) { return; }
for (auto y_px = u32{ 0 }; y_px < 8; ++y_px)
{
@@ -24,16 +25,16 @@ namespace nes::app
auto const c = tile.data[y_px * 8 + x_px];
if (c != color::transparent)
{
- display_.set(8 * x + x_px, 8 * y + y_px, resolve_color(c));
+ display_.set(8 * static_cast<u32>(x) + x_px, 8 * static_cast<u32>(y) + y_px, resolve_color(c));
}
}
}
}
- auto renderer::render_mask_tile(u32 const x, u32 const y, mask_tile const tile, color const c) -> void
+ auto renderer::render_mask_tile(i32 const x, i32 const y, mask_tile const tile, color const c) -> void
{
if (c == color::transparent) { return; }
- if (x >= width || y >= height) { return; }
+ if (x < 0 || y < 0 || x >= static_cast<i32>(width) || y >= static_cast<i32>(height)) { return; }
auto const resolved = resolve_color(c);
for (auto y_px = u32{ 0 }; y_px < 8; ++y_px)
@@ -43,47 +44,103 @@ namespace nes::app
{
if (row & 0b10000000)
{
- display_.set(8 * x + x_px, 8 * y + y_px, resolved);
+ display_.set(8 * static_cast<u32>(x) + x_px, 8 * static_cast<u32>(y) + y_px, resolved);
}
row <<= 1;
}
}
}
- auto renderer::render_image(u32 const x, u32 const y, image_view const image) -> void
+ auto renderer::render_image(i32 const x, i32 const y, image_view const image) -> void
{
for (auto y_img = u32{ 0 }; y_img < image.get_height(); ++y_img)
{
for (auto x_img = u32{ 0 }; x_img < image.get_width(); ++x_img)
{
- render_image_tile(x + x_img, y + y_img, image.get(x_img, y_img));
+ render_image_tile(x + static_cast<i32>(x_img), y + static_cast<i32>(y_img), image.get(x_img, y_img));
}
}
}
- auto renderer::render_text(u32 const x, u32 const y, std::string_view const str, color const c) -> void
+ auto renderer::render_text(
+ i32 x, i32 const y, std::string_view const str, color const c, text_attributes const attributes) -> void
{
- for (auto i = u32{ 0 }; i < str.size(); ++i)
+ // Enforce max length
+ char buffer[width];
+ auto const length = std::min(static_cast<u32>(str.length()), std::min(width, attributes.get_max_width()));
+ auto const ellipsize_dots = std::min(length, u32{ 3 });
+
+ // Enforce ellipsize mode
+ if (str.length() > length)
+ {
+ switch (attributes.get_ellipsize_mode())
+ {
+ case ellipsize_mode::clip:
+ {
+ std::copy_n(str.begin(), length, buffer);
+ break;
+ }
+ case ellipsize_mode::end:
+ {
+ std::copy_n(str.begin(), length, buffer);
+ for (auto i = u32{ 0 }; i < ellipsize_dots; ++i) { buffer[length - 1 - i] = '.'; }
+ break;
+ }
+ case ellipsize_mode::start:
+ {
+ std::copy_n(str.begin() + str.length() - length, length, buffer);
+ for (auto i = u32{ 0 }; i < ellipsize_dots; ++i) { buffer[i] = '.'; }
+ break;
+ }
+ case ellipsize_mode::middle:
+ {
+ auto const visible = length - ellipsize_dots;
+ auto const start = visible / 2;
+ auto const end = visible - start;
+ std::copy_n(str.begin(), start, buffer);
+ for (auto i = u32{ 0 }; i < ellipsize_dots; ++i) { buffer[start + i] = '.'; }
+ std::copy_n(str.begin() + str.length() - end, end, buffer + start + ellipsize_dots);
+ break;
+ }
+ }
+ }
+ else
+ {
+ std::copy_n(str.begin(), length, buffer);
+ }
+
+ // Enforce alignment
+ switch (attributes.get_alignment())
+ {
+ case text_alignment::left: break;
+ case text_alignment::center: x -= length / 2; break;
+ case text_alignment::right: x -= length; break;
+ }
+
+ // Draw the text
+ for (auto i = u32{ 0 }; i < length; ++i)
{
- auto const character = str[i];
- render_mask_tile(x + i, y, resolve_character(str[i]), c);
+ auto const character = static_cast<u32>(buffer[i]);
+ render_mask_tile(x + static_cast<i32>(i), y, resolve_character(character), c);
}
}
auto renderer::render_rect(
- u32 const x, u32 const y, u32 const width, u32 const height, color const c) -> void
+ i32 const x, i32 const y, u32 const width, u32 const height, color const c) -> void
{
if (c == color::transparent) { return; }
- if (x >= width || y >= height) { return; }
+ if (x >= static_cast<i32>(width) || y >= static_cast<i32>(height)) { return; }
auto const resolved = resolve_color(c);
- auto const x_stop = std::min(x + width, renderer::width);
- auto const y_stop = std::min(y + height, renderer::height);
- for (auto y_px = u32{ y * 8 }; y_px < y_stop * 8; ++y_px)
+ auto const x_stop = std::min(x + static_cast<i32>(width), static_cast<i32>(renderer::width));
+ auto const y_stop = std::min(y + static_cast<i32>(height), static_cast<i32>(renderer::height));
+ auto const x_start = std::max(x, 0);
+ auto const y_start = std::max(y, 0);
+ for (auto y_px = i32{ y_start * 8 }; y_px < y_stop * 8; ++y_px)
{
- for (auto x_px = u32{ x * 8 }; x_px < x_stop * 8; ++x_px)
+ for (auto x_px = i32{ x_start * 8 }; x_px < x_stop * 8; ++x_px)
{
- display_.set(x_px, y_px, resolved);
+ display_.set(static_cast<u32>(x_px), static_cast<u32>(y_px), resolved);
}
}
}
diff --git a/lib/nes/app/graphics/renderer.hh b/lib/nes/app/graphics/renderer.hh
index 08e4225..cc0b9af 100644
--- a/lib/nes/app/graphics/renderer.hh
+++ b/lib/nes/app/graphics/renderer.hh
@@ -1,6 +1,9 @@
#pragma once
#include "nes/common/types.hh"
+#include "nes/app/graphics/color.hh"
+#include "nes/app/graphics/text-attributes.hh"
+#include "nes/common/string-builder.hh"
#include <string_view>
namespace nes
@@ -14,7 +17,6 @@ namespace nes::app
struct image_tile;
struct mask_tile;
class image_view;
- enum class color : u8;
/// Renders tiles and other primitive shapes onto a display.
///
@@ -41,18 +43,33 @@ namespace nes::app
auto set_border(u32 px, color) -> void;
/// Render a colored tile at the given tile coordinates.
- auto render_image_tile(u32 x, u32 y, image_tile const&) -> void;
+ auto render_image_tile(i32 x, i32 y, image_tile const&) -> void;
/// Render an alpha-only tile at the given tile coordinates with the given color.
- auto render_mask_tile(u32 x, u32 y, mask_tile, color) -> void;
+ auto render_mask_tile(i32 x, i32 y, mask_tile, color) -> void;
/// Render an image at the given tile coordinates.
- auto render_image(u32 x, u32 y, image_view) -> void;
+ auto render_image(i32 x, i32 y, image_view) -> void;
/// Render a string of text at the given tile coordinates (one tile per character).
- auto render_text(u32 x, u32 y, std::string_view, color) -> void;
- /// Render a string of text at the given tile coordinates, limiting its length to the given width and
- /// ellipsizing afterward.
- auto render_text(u32 x, u32 y, std::string_view, u32 width, color) -> void;
+ auto render_text(i32 x, i32 y, std::string_view, color, text_attributes = text_attributes{}) -> void;
+
+ template<typename... Args>
+ auto render_text_format(
+ i32 const x, i32 const y, color const c, text_attributes const attrs, std::string_view const fmt,
+ Args... args) -> void
+ {
+ char buffer[width];
+ auto const str = string_builder{ buffer, sizeof(buffer) }.append_format(fmt, args...).get_result();
+ render_text(x, y, str, c, attrs);
+ }
+
+ template<typename... Args>
+ auto render_text_format(
+ i32 const x, i32 const y, color const c, std::string_view const fmt, Args... args) -> void
+ {
+ render_text_format(x, y, c, text_attributes{}, fmt, args...);
+ }
+
/// Render a rectangle at the given tile coordinates with a given size (measured in tiles).
- auto render_rect(u32 x, u32 y, u32 width, u32 height, color) -> void;
+ auto render_rect(i32 x, i32 y, u32 width, u32 height, color) -> void;
/// Completely fill the screen.
auto render_fill(color) -> void;
diff --git a/lib/nes/app/graphics/text-attributes.hh b/lib/nes/app/graphics/text-attributes.hh
new file mode 100644
index 0000000..6c82311
--- /dev/null
+++ b/lib/nes/app/graphics/text-attributes.hh
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "nes/common/types.hh"
+
+namespace nes::app
+{
+ enum class ellipsize_mode
+ {
+ clip,
+ start,
+ middle,
+ end,
+ };
+
+ enum class text_alignment
+ {
+ left,
+ center,
+ right,
+ };
+
+ class text_attributes
+ {
+ u32 max_width_{ 32 };
+ ellipsize_mode ellipsize_mode_{ ellipsize_mode::clip };
+ text_alignment alignment_{ text_alignment::left };
+
+ public:
+ explicit text_attributes() = default;
+
+ auto get_max_width() const -> u32 { return max_width_; }
+ auto get_ellipsize_mode() const -> ellipsize_mode { return ellipsize_mode_; }
+ auto get_alignment() const -> text_alignment { return alignment_; }
+
+ auto set_max_width(u32 const v) -> text_attributes& { max_width_ = v; return *this; }
+ auto set_ellipsize_mode(ellipsize_mode const v) -> text_attributes& { ellipsize_mode_ = v; return *this; }
+ auto set_alignment(text_alignment const v) -> text_attributes& { alignment_ = v; return *this; }
+ };
+} // namespace nes::app \ No newline at end of file
diff --git a/lib/nes/app/input/input-buffer.hh b/lib/nes/app/input/input-buffer.hh
index 68fca6b..319cfa9 100644
--- a/lib/nes/app/input/input-buffer.hh
+++ b/lib/nes/app/input/input-buffer.hh
@@ -7,6 +7,7 @@ namespace nes::app
{
class input_event;
+ /// A buffer which stores the "pressed" state for each key and a queue of key-down and key-up events.
class input_buffer
{
struct event final : list<event>::node
diff --git a/lib/nes/app/input/input-event.hh b/lib/nes/app/input/input-event.hh
index 90e67b0..26d6448 100644
--- a/lib/nes/app/input/input-event.hh
+++ b/lib/nes/app/input/input-event.hh
@@ -4,6 +4,7 @@
namespace nes::app
{
+ /// A keyboard input event which can be polled.
class input_event
{
public:
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