summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/mac/scene.mm103
-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
-rw-r--r--lib/nes/common/CMakeLists.txt4
-rw-r--r--lib/nes/common/string-builder.cc255
-rw-r--r--lib/nes/common/string-builder.hh98
19 files changed, 915 insertions, 57 deletions
diff --git a/app/mac/scene.mm b/app/mac/scene.mm
index bf1a61b..031413f 100644
--- a/app/mac/scene.mm
+++ b/app/mac/scene.mm
@@ -6,6 +6,10 @@
#import "nes/common/display.hh"
#import <memory>
#import <optional>
+#import <fcntl.h>
+#import <unistd.h>
+#import <termios.h>
+#import <sys/ioctl.h>
namespace
{
@@ -252,12 +256,108 @@ namespace
return res;
}
};
+
+ class input_device_controller_serial : public nes::app::input_device_controller
+ {
+ char const* path_;
+ int fd_{ 0 };
+ nes::sys::button_mask current_{};
+ index index_{ index::unused };
+
+ public:
+ explicit input_device_controller_serial(char const* path)
+ : path_{ path }
+ {
+ fd_ = open(path, O_RDONLY | O_NOCTTY | O_NDELAY | O_NONBLOCK);
+ if (fd_ == -1)
+ {
+ perror("open controller");
+ return;
+ }
+
+ if (!setup()) { close(); }
+ }
+
+ ~input_device_controller_serial() override
+ {
+ close();
+ }
+
+ auto read_buttons() -> nes::sys::button_mask override
+ {
+ if (fd_ == -1) { return nes::sys::button_mask{}; }
+
+ // Poll for any updates until we can't read anymore.
+ while (true)
+ {
+ auto buf = nes::u8{};
+ auto res = read(fd_, &buf, 1);
+ if (res == -1)
+ {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) { break; }
+
+ perror("read from controller");
+ close();
+ break;
+ }
+
+ current_ = nes::sys::button_mask::from_raw_value(buf);
+ }
+
+ return current_;
+ }
+
+ auto is_reliable() const -> bool override { return fd_ != -1; }
+ auto get_name() const -> std::string_view override { return path_; }
+ auto set_index(index const index) -> void override { index_ = index; }
+ auto get_index() const -> index override { return index_; }
+
+ private:
+ auto close() -> void
+ {
+ if (fd_ != -1)
+ {
+ ::close(fd_);
+ fd_ = -1;
+ }
+ }
+
+ auto setup() -> bool
+ {
+ if (ioctl(fd_, TIOCEXCL) == -1)
+ {
+ perror("ioctl for controller");
+ return false;
+ }
+
+ auto attr = termios{};
+ if (tcgetattr(fd_, &attr) == -1)
+ {
+ perror("tcgetattr for controller");
+ return false;
+ }
+
+ cfsetispeed(&attr, B57600);
+ cfsetospeed(&attr, B57600);
+ attr.c_cflag = attr.c_cflag | CLOCAL | CREAD;
+ attr.c_cflag = (attr.c_cflag & static_cast<tcflag_t>(~CSIZE)) | CS8;
+
+ if (tcsetattr(fd_, TCSANOW, &attr) == -1)
+ {
+ perror("tcsetattr for controller");
+ return false;
+ }
+
+ return true;
+ }
+ };
} // namespace
@implementation Scene
{
std::unique_ptr<display_impl> _display;
std::unique_ptr<input_device_keyboard_impl> _keyboard;
+ std::unique_ptr<input_device_controller_serial> _serialController;
std::vector<std::unique_ptr<input_device_controller_impl>> _controllers;
std::unique_ptr<nes::app::application> _app;
std::optional<NSTimeInterval> _lastTimestamp;
@@ -277,8 +377,11 @@ namespace
_display = std::make_unique<display_impl>(node);
_keyboard = std::make_unique<input_device_keyboard_impl>([[GCKeyboard coalescedKeyboard] keyboardInput]);
+ _serialController = std::make_unique<input_device_controller_serial>("/dev/cu.usbserial-FTB6SPL3");
_app = std::make_unique<nes::app::application>(*_display, *_keyboard);
+ _app->add_controller(*_serialController);
+
for (GCController* controller in [GCController controllers])
{
auto gamepad = [controller extendedGamepad];
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
diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt
index bd83a57..11143d7 100644
--- a/lib/nes/common/CMakeLists.txt
+++ b/lib/nes/common/CMakeLists.txt
@@ -6,4 +6,6 @@ target_sources(
rgb.hh
list.hh
box.hh
- display.hh) \ No newline at end of file
+ display.hh
+ string-builder.hh
+ string-builder.cc) \ No newline at end of file
diff --git a/lib/nes/common/string-builder.cc b/lib/nes/common/string-builder.cc
new file mode 100644
index 0000000..efc1326
--- /dev/null
+++ b/lib/nes/common/string-builder.cc
@@ -0,0 +1,255 @@
+#include "nes/common/string-builder.hh"
+
+namespace nes
+{
+ string_builder::string_builder(char* buffer, u32 buffer_length)
+ : buffer_{ buffer }
+ , buffer_length_{ buffer_length }
+ {
+ }
+
+ auto string_builder::append_char(char const value) -> string_builder&
+ {
+ if (actual_length_ == buffer_length_)
+ {
+ is_good_ = false;
+ return *this;
+ }
+
+ buffer_[actual_length_] = value;
+ actual_length_ += 1;
+ return *this;
+ }
+
+ auto string_builder::append_string(std::string_view const value) -> string_builder&
+ {
+ for (auto const c : value)
+ {
+ append_char(c);
+ }
+
+ return *this;
+ }
+
+ auto string_builder::append_int(i64 value, number_format const format) -> string_builder&
+ {
+ if (value < 0)
+ {
+ append_char('-');
+ value += 1; // to prevent lossy conversion to unsigned
+ auto abs_value = static_cast<u64>(-value);
+ abs_value += 1; // to correct the previous offset
+ append_int(abs_value, format);
+ }
+ else
+ {
+ append_int(static_cast<u64>(value), format);
+ }
+
+ return *this;
+ }
+
+ auto string_builder::append_int(u64 value, number_format const format) -> string_builder&
+ {
+ constexpr auto max_string_length = u32{ 64 }; // worst case is a 64-bit integer in binary representation
+ constexpr auto digits = "0123456789abcdef";
+
+ auto base = u64{ 10 };
+ switch (format)
+ {
+ case number_format::binary:
+ append_string("0b");
+ base = 2;
+ break;
+ case number_format::octal:
+ append_string("0");
+ base = 8;
+ break;
+ case number_format::decimal:
+ break;
+ case number_format::hexadecimal:
+ append_string("0x");
+ base = 16;
+ break;
+ }
+
+ char buf[max_string_length + 1] = {}; // null-terminated
+ auto pos = max_string_length;
+ // Go through each digit in reverse order by continuously dividing the integer by the base.
+ while (value)
+ {
+ pos--;
+
+ buf[pos] = digits[value % base]; // Convert digit to char representation
+ value /= base;
+ }
+ if (pos == max_string_length)
+ {
+ // Special case 0 would not print any character otherwise.
+ pos--;
+ buf[pos] = '0';
+ }
+
+ append_string(&buf[pos]);
+ return *this;
+ }
+
+ auto string_builder::append_bool(bool const value) -> string_builder&
+ {
+ append_string(value ? "true" : "false");
+ return *this;
+ }
+
+ auto string_builder::append_format(
+ std::string_view const fmt, format_arg const* args, u32 const arg_count) -> string_builder&
+ {
+ // Go through the format string, inserting the arguments for the templates as necessary.
+ auto current_arg = u32{ 0 };
+ for (auto i = u32{ 0 }; i < fmt.length(); ++i)
+ {
+ if (fmt[i] == '{')
+ {
+ i += 1;
+
+ if (fmt[i] == '{') {
+ // escaped '{'
+ append_char('{');
+ }
+ else
+ {
+ // start of template
+ auto const params = &fmt[i]; // remember start position
+ auto params_length = u32{ 0 };
+ // skip to the end of the template
+ while (fmt[i] && fmt[i] != '}')
+ {
+ params_length++;
+ i++;
+ }
+ if (fmt[i] != '}')
+ {
+ // unmatched '{'
+ is_good_ = false;
+ break;
+ }
+
+ // end of template
+ auto const p = std::string_view{ params, params_length };
+ if (current_arg < arg_count)
+ {
+ append_format_arg(args[current_arg], p);
+ current_arg += 1;
+ }
+ else
+ {
+ // missing argument in parameter pack
+ is_good_ = false;
+ }
+ }
+ }
+ else if (fmt[i] == '}')
+ {
+ i += 1;
+
+ if (fmt[i] == '}')
+ {
+ // escaped '}'
+ append_char('}');
+ }
+ else
+ {
+ // unmatched '}'
+ is_good_ = false;
+ break;
+ }
+ }
+ else
+ {
+ // normal character in the template string
+ append_char(fmt[i]);
+ }
+ }
+
+ if (current_arg < arg_count)
+ {
+ // not all supplied arguments were used
+ is_good_ = false;
+ }
+
+ return *this;
+ }
+
+ auto string_builder::append_format_arg(format_arg const arg, std::string_view const params) -> string_builder&
+ {
+ auto const parse_params = [&]
+ {
+ if (!params.empty())
+ {
+ is_good_ = false;
+ }
+ };
+
+ auto const parse_params_integer = [&](number_format& out_format)
+ {
+ if (params.empty() || params == "d")
+ {
+ out_format = number_format::decimal;
+ }
+ else if (params == "x")
+ {
+ out_format = number_format::hexadecimal;
+ }
+ else if (params == "o")
+ {
+ out_format = number_format::octal;
+ }
+ else if (params == "b")
+ {
+ out_format = number_format::binary;
+ }
+ else
+ {
+ out_format = number_format::decimal;
+ is_good_ = false;
+ }
+ };
+
+ switch (arg.type)
+ {
+ case format_arg_type::string:
+ {
+ parse_params();
+ append_string(arg.string);
+ break;
+ }
+ case format_arg_type::character:
+ {
+ parse_params();
+ append_char(arg.character);
+ break;
+ }
+ case format_arg_type::signed_integer:
+ {
+ auto format = number_format{};
+ parse_params_integer(format);
+ append_int(arg.signed_integer, format);
+ break;
+ }
+ case format_arg_type::unsigned_integer:
+ {
+ auto format = number_format{};
+ parse_params_integer(format);
+ append_int(arg.unsigned_integer, format);
+ break;
+ }
+ case format_arg_type::boolean:
+ {
+ parse_params();
+ append_bool(arg.character);
+ break;
+ }
+ }
+
+ return *this;
+ }
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/common/string-builder.hh b/lib/nes/common/string-builder.hh
new file mode 100644
index 0000000..fda85a4
--- /dev/null
+++ b/lib/nes/common/string-builder.hh
@@ -0,0 +1,98 @@
+#pragma once
+
+#include "nes/common/types.hh"
+#include <string_view>
+
+namespace nes
+{
+ enum class number_format
+ {
+ decimal,
+ hexadecimal,
+ octal,
+ binary,
+ };
+
+ /// Builds a string into a fixed-length buffer.
+ class string_builder
+ {
+ char* buffer_{ nullptr };
+ u32 buffer_length_{ 0 };
+ u32 actual_length_{ 0 };
+ bool is_good_{ true };
+
+ enum class format_arg_type
+ {
+ string,
+ character,
+ signed_integer,
+ unsigned_integer,
+ boolean,
+ };
+
+ struct format_arg
+ {
+ format_arg_type type;
+ union
+ {
+ std::string_view string;
+ char character;
+ i64 signed_integer;
+ u64 unsigned_integer;
+ bool boolean;
+ };
+
+ format_arg() = delete;
+
+ explicit format_arg(char const* v) : type{ format_arg_type::string } { string = v; }
+ explicit format_arg(std::string_view const v) : type{ format_arg_type::string } { string = v; }
+ explicit format_arg(char const v) : type{ format_arg_type::character } { character = v; }
+ explicit format_arg(unsigned char const v) : format_arg{ static_cast<char>(v) } {}
+ explicit format_arg(long long const v) : type{ format_arg_type::signed_integer } { signed_integer = v; }
+ explicit format_arg(unsigned long long const v) : type{ format_arg_type::unsigned_integer } { unsigned_integer = v; }
+ explicit format_arg(short const v) : format_arg{ static_cast<signed long long>(v) } {}
+ explicit format_arg(unsigned short const v) : format_arg{ static_cast<unsigned long long>(v) } {}
+ explicit format_arg(int const v) : format_arg{ static_cast<signed long long>(v) } {}
+ explicit format_arg(unsigned int const v) : format_arg{ static_cast<unsigned long long>(v) } {}
+ explicit format_arg(long const v) : format_arg{ static_cast<signed long long>(v) } {}
+ explicit format_arg(unsigned long const v) : format_arg{ static_cast<unsigned long long>(v) } {}
+ explicit format_arg(bool const v) : type{ format_arg_type::boolean } { boolean = v; }
+ };
+
+ public:
+ explicit string_builder(char* buffer, u32 buffer_length);
+
+ /// Get the resulting string.
+ auto get_result() const -> std::string_view { return std::string_view{ buffer_, actual_length_ }; }
+
+ /// Returns false after an invalid format string or buffer overflow.
+ auto is_good() const -> bool { return is_good_; }
+
+ auto append_char(char) -> string_builder&;
+ auto append_string(std::string_view) -> string_builder&;
+ auto append_int(i64, number_format = number_format::decimal) -> string_builder&;
+ auto append_int(u64, number_format = number_format::decimal) -> string_builder&;
+ auto append_bool(bool) -> string_builder&;
+
+ /// Print a templated string.
+ ///
+ /// Placeholders are signified by `{<params>}` where `params` contains optional formatting parameters:
+ /// - for integers either "d" (the default), "x", "o", or "b"
+ /// - for any other type, no parameters are supported.
+ ///
+ /// Curly braces can be escaped by repeating them twice like {{this}}.
+ template<typename... Args>
+ auto append_format(std::string_view const fmt, Args... args) -> string_builder&
+ {
+ constexpr u32 arg_count = sizeof...(Args);
+ // Add another argument at the end so we don't have an empty array expression.
+ const format_arg arg_wrappers[arg_count + 1] = { format_arg{ args }..., format_arg(0) };
+
+ return append_format(fmt, arg_wrappers, arg_count);
+ }
+
+ private:
+ auto append_format(std::string_view fmt, format_arg const* args, u32 arg_count) -> string_builder&;
+ auto append_format_arg(format_arg, std::string_view params) -> string_builder&;
+ };
+} // namespace nes \ No newline at end of file