UI: Extract a Document class

This commit is contained in:
Luke Street
2026-04-30 13:44:33 -06:00
parent a1960eaa33
commit 7a438ad30f
10 changed files with 267 additions and 245 deletions
+2
View File
@@ -1466,6 +1466,8 @@ set(DUSK_FILES
src/dusk/ui/button.hpp
src/dusk/ui/component.cpp
src/dusk/ui/component.hpp
src/dusk/ui/document.cpp
src/dusk/ui/document.hpp
src/dusk/ui/editor.cpp
src/dusk/ui/editor.hpp
src/dusk/ui/event.cpp
+71
View File
@@ -0,0 +1,71 @@
#include "document.hpp"
#include "aurora/rmlui.hpp"
#include "ui.hpp"
namespace dusk::ui {
namespace {
Rml::ElementDocument* load_document(const Rml::String& path) {
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return nullptr;
}
return context->LoadDocument(path);
}
} // namespace
Document::Document(const Rml::String& path) : mDocument(load_document(path)) {
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
event.StopPropagation();
}
});
}
Document::~Document() {
mListeners.clear();
if (mDocument != nullptr) {
mDocument->Close();
mDocument = nullptr;
}
}
void Document::show() {
if (mDocument != nullptr) {
mDocument->Show();
focus();
}
}
void Document::hide() {
if (mDocument != nullptr) {
mDocument->Hide();
}
}
void Document::update() {}
bool Document::focus() {
return false;
}
void Document::listen(Rml::Element* element, Rml::EventId event,
ScopedEventListener::Callback callback, bool capture) {
if (element == nullptr) {
element = mDocument;
}
if (element == nullptr || !callback) {
return;
}
mListeners.emplace_back(
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
}
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
return false;
}
} // namespace dusk::ui
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "component.hpp"
#include "ui.hpp"
namespace dusk::ui {
class Document {
public:
Document(const Rml::String& path);
virtual ~Document();
Document(const Document&) = delete;
Document& operator=(const Document&) = delete;
virtual void show();
virtual void hide();
virtual void update();
virtual bool focus();
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
bool capture = false);
void listen(Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) {
listen(mDocument, event, std::move(callback), capture);
}
protected:
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
Rml::ElementDocument* mDocument;
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
};
} // namespace dusk::ui
+44 -70
View File
@@ -3,6 +3,8 @@
#include <RmlUi/Core.h>
#include "aurora/rmlui.hpp"
#include "editor.hpp"
#include "settings.hpp"
#include "tab_button.hpp"
#include "ui.hpp"
#include "window.hpp"
@@ -10,22 +12,10 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <utility>
namespace dusk::ui {
Popup::Popup(Window& settingsWindow, Window& editorWindow)
: mSettingsWindow(settingsWindow), mEditorWindow(editorWindow) {
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
mDocument = context->LoadDocument("res/rml/popup.rml");
if (mDocument == nullptr) {
return;
}
Popup::Popup() : Document("res/rml/popup.rml") {
auto* tabBar = mDocument->GetElementById("tab-bar");
if (tabBar == nullptr) {
return;
@@ -43,72 +33,32 @@ Popup::Popup(Window& settingsWindow, Window& editorWindow)
mTabActions = {
[this] {
hide();
mSettingsWindow.show();
mSettingsWindow.focus_for_input();
// TODO: make this better
auto& settingsWindow = add_document(std::make_unique<SettingsWindow>());
settingsWindow.show();
set_selected_tab(0);
},
[this] {
set_selected_tab(1);
},
[this] { set_selected_tab(1); },
[this] {
hide();
mEditorWindow.show();
mEditorWindow.focus_for_input();
// TODO: make this better
auto& editorWindow = add_document(std::make_unique<EditorWindow>());
editorWindow.show();
set_selected_tab(2);
},
[this] {
set_selected_tab(3);
},
[this] {
set_selected_tab(4);
},
[this] { set_selected_tab(3); },
[this] { set_selected_tab(4); },
};
mTabs.reserve(tabLabels.size());
for (int i = 0; i < static_cast<int>(tabLabels.size()); ++i) {
mTabs.push_back(create_tab_button(tabBar, tabLabels[i], i == mSelectedTabIndex,
[this, i](Rml::Event&) {
mTabs.push_back(
create_tab_button(tabBar, tabLabels[i], i == mSelectedTabIndex, [this, i](Rml::Event&) {
if (i >= 0 && i < static_cast<int>(mTabActions.size())) {
mTabActions[i]();
}
}));
}
mKeyListener = std::make_unique<ScopedEventListener>(
mDocument, Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd == NavCommand::None) {
return;
}
if (cmd == NavCommand::Left || cmd == NavCommand::Previous) {
focus_tab(std::max(0, mSelectedTabIndex - 1));
event.StopPropagation();
return;
}
if (cmd == NavCommand::Right || cmd == NavCommand::Next) {
focus_tab(std::min(static_cast<int>(mTabs.size()) - 1, mSelectedTabIndex + 1));
event.StopPropagation();
return;
}
if (cmd == NavCommand::Confirm && mSelectedTabIndex >= 0 &&
mSelectedTabIndex < static_cast<int>(mTabActions.size()))
{
mTabActions[mSelectedTabIndex]();
event.StopPropagation();
return;
}
if (cmd == NavCommand::Cancel) {
hide();
event.StopPropagation();
}
});
}
Popup::~Popup() {
auto* context = aurora::rmlui::get_context();
if (context != nullptr && mDocument != nullptr) {
context->UnloadDocument(mDocument);
}
}
void Popup::show() {
@@ -117,7 +67,7 @@ void Popup::show() {
}
mHideDeadline.reset();
mDocument->Show();
Document::show();
mVisible = true;
}
@@ -129,9 +79,11 @@ void Popup::hide() {
if (auto* popup = mDocument->GetElementById("popup")) {
popup->SetClass("popup-hidden", true);
mHideDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); // Must match the transition duration in popup.rcss
mHideDeadline =
std::chrono::steady_clock::now() +
std::chrono::milliseconds(500); // Must match the transition duration in popup.rcss
} else {
mDocument->Hide();
Document::hide();
}
mVisible = false;
}
@@ -148,7 +100,29 @@ bool Popup::is_visible() const {
return mVisible;
}
void Popup::update() noexcept {
bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Left || cmd == NavCommand::Previous) {
focus_tab(std::max(0, mSelectedTabIndex - 1));
return true;
}
if (cmd == NavCommand::Right || cmd == NavCommand::Next) {
focus_tab(std::min(static_cast<int>(mTabs.size()) - 1, mSelectedTabIndex + 1));
return true;
}
if (cmd == NavCommand::Confirm && mSelectedTabIndex >= 0 &&
mSelectedTabIndex < static_cast<int>(mTabActions.size()))
{
mTabActions[mSelectedTabIndex]();
return true;
}
if (cmd == NavCommand::Cancel) {
hide();
return true;
}
return false;
}
void Popup::update() {
if (mDocument == nullptr) {
return;
}
@@ -164,7 +138,7 @@ void Popup::update() noexcept {
for (const auto& tab : mTabs) {
tabs.push_back(tab.get());
}
dusk::ui::set_selected_tab(tabs, mSelectedTabIndex);
ui::set_selected_tab(tabs, mSelectedTabIndex);
}
void Popup::set_selected_tab(int index) {
@@ -177,7 +151,7 @@ void Popup::set_selected_tab(int index) {
for (const auto& tab : mTabs) {
tabs.push_back(tab.get());
}
dusk::ui::set_selected_tab(tabs, mSelectedTabIndex);
ui::set_selected_tab(tabs, mSelectedTabIndex);
}
bool Popup::focus_tab(int index) {
+10 -14
View File
@@ -1,8 +1,7 @@
#pragma once
#include <RmlUi/Core/ElementDocument.h>
#include "button.hpp"
#include "document.hpp"
#include "event.hpp"
#include <chrono>
@@ -12,36 +11,33 @@
namespace dusk::ui {
class Window;
class Popup {
class Popup : public Document {
public:
Popup(Window& settingsWindow, Window& editorWindow);
~Popup();
Popup();
Popup(const Popup&) = delete;
Popup& operator=(const Popup&) = delete;
void show();
void hide();
void show() override;
void hide() override;
void update() override;
void toggle();
bool is_visible() const;
void update() noexcept;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
void set_selected_tab(int index);
bool focus_tab(int index);
Window& mSettingsWindow;
Window& mEditorWindow;
Rml::ElementDocument* mDocument = nullptr;
std::vector<std::unique_ptr<Button> > mTabs;
std::vector<std::function<void()> > mTabActions;
std::unique_ptr<Button> mCloseButton;
int mSelectedTabIndex = 0;
bool mVisible = false;
std::optional<std::chrono::steady_clock::time_point> mHideDeadline;
std::unique_ptr<ScopedEventListener> mKeyListener;
};
} // namespace dusk::ui
+49 -20
View File
@@ -4,9 +4,10 @@
#include <SDL3/SDL_filesystem.h>
#include <aurora/rmlui.hpp>
#include <algorithm>
#include <filesystem>
#include "popup.hpp"
#include "aurora/lib/window.hpp"
#include "window.hpp"
namespace dusk::ui {
@@ -17,8 +18,7 @@ void load_font(const char* filename, bool fallback = false) {
}
bool sInitialized = false;
std::vector<std::unique_ptr<Window> > sWindows;
std::unique_ptr<Popup> sPopup;
std::vector<std::unique_ptr<Document> > sDocuments;
} // namespace
@@ -39,8 +39,7 @@ bool initialize() noexcept {
}
void shutdown() noexcept {
sPopup.reset();
sWindows.clear();
sDocuments.clear();
sInitialized = false;
}
@@ -48,27 +47,23 @@ void handle_event(const SDL_Event& event) noexcept {
// TODO
}
Window& add_window(std::unique_ptr<Window> window) noexcept {
Window& ret = *window;
sWindows.push_back(std::move(window));
Document& add_document(std::unique_ptr<Document> doc) noexcept {
Document& ret = *doc;
sDocuments.push_back(std::move(doc));
return ret;
}
void remove_window(Window& window) noexcept {
// TODO
}
Popup& add_popup(std::unique_ptr<Popup> popupMenu) noexcept {
sPopup = std::move(popupMenu);
return *sPopup;
void remove_document(Document& doc) noexcept {
const auto it = std::find_if(sDocuments.begin(), sDocuments.end(),
[&doc](const std::unique_ptr<Document>& current) { return current.get() == &doc; });
if (it != sDocuments.end()) {
sDocuments.erase(it);
}
}
void update() noexcept {
for (const auto& window : sWindows) {
window->update();
}
if (sPopup != nullptr) {
sPopup->update();
for (const auto& doc : sDocuments) {
doc->update();
}
}
@@ -133,4 +128,38 @@ NavCommand map_nav_event(const Rml::Event& event) noexcept {
}
}
Insets safe_area_insets(Rml::Context* context) noexcept {
if (context == nullptr) {
return {};
}
auto* window = aurora::window::get_sdl_window();
if (window == nullptr) {
return {};
}
const AuroraWindowSize windowSize = aurora::window::get_window_size();
if (windowSize.width == 0 || windowSize.height == 0) {
return {};
}
SDL_Rect safeRect{};
if (!SDL_GetWindowSafeArea(window, &safeRect)) {
return {};
}
const Rml::Vector2i contextSize = context->GetDimensions();
const float scaleX = static_cast<float>(contextSize.x) / static_cast<float>(windowSize.width);
const float scaleY = static_cast<float>(contextSize.y) / static_cast<float>(windowSize.height);
const float safeRight = static_cast<float>(safeRect.x + safeRect.w);
const float safeBottom = static_cast<float>(safeRect.y + safeRect.h);
return {
.top = std::max(0.0f, static_cast<float>(safeRect.y)) * scaleY,
.right = std::max(0.0f, static_cast<float>(windowSize.width) - safeRight) * scaleX,
.bottom = std::max(0.0f, static_cast<float>(windowSize.height) - safeBottom) * scaleY,
.left = std::max(0.0f, static_cast<float>(safeRect.x)) * scaleX,
};
}
} // namespace dusk::ui
+21 -4
View File
@@ -1,23 +1,39 @@
#pragma once
#include <RmlUi/Core/Event.h>
#include <RmlUi/Core.h>
#include <SDL3/SDL_events.h>
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
#include "nav_types.hpp"
namespace dusk::ui {
class Window;
class Document;
class Popup;
struct Insets {
float top = 0.0f;
float right = 0.0f;
float bottom = 0.0f;
float left = 0.0f;
bool operator==(const Insets& other) const noexcept {
return top == other.top && right == other.right && bottom == other.bottom &&
left == other.left;
}
};
bool initialize() noexcept;
void shutdown() noexcept;
void handle_event(const SDL_Event& event) noexcept;
void update() noexcept;
Window& add_window(std::unique_ptr<Window> window) noexcept;
void remove_window(Window& window) noexcept;
Document& add_document(std::unique_ptr<Document> doc) noexcept;
void remove_document(Document& doc) noexcept;
Popup& add_popup(std::unique_ptr<Popup> popup) noexcept;
@@ -25,5 +41,6 @@ std::filesystem::path resource_path(const std::filesystem::path& filename) noexc
std::string escape(std::string_view str) noexcept;
NavCommand map_nav_event(const Rml::Event& event) noexcept;
Insets safe_area_insets(Rml::Context* context) noexcept;
} // namespace dusk::ui
+27 -110
View File
@@ -1,8 +1,5 @@
#include "window.hpp"
#include <RmlUi/Core.h>
#include <SDL3/SDL_video.h>
#include "aurora/lib/window.hpp"
#include "aurora/rmlui.hpp"
#include "magic_enum.hpp"
@@ -27,113 +24,22 @@ float base_body_padding(Rml::Context* context) noexcept {
return 64.0f * dpRatio;
}
Window::Insets safe_area_insets(Rml::Context* context) noexcept {
if (context == nullptr) {
return {};
}
auto* window = aurora::window::get_sdl_window();
if (window == nullptr) {
return {};
}
const AuroraWindowSize windowSize = aurora::window::get_window_size();
if (windowSize.width == 0 || windowSize.height == 0) {
return {};
}
SDL_Rect safeRect{};
if (!SDL_GetWindowSafeArea(window, &safeRect)) {
return {};
}
const Rml::Vector2i contextSize = context->GetDimensions();
const float scaleX = static_cast<float>(contextSize.x) / static_cast<float>(windowSize.width);
const float scaleY = static_cast<float>(contextSize.y) / static_cast<float>(windowSize.height);
const float safeRight = static_cast<float>(safeRect.x + safeRect.w);
const float safeBottom = static_cast<float>(safeRect.y + safeRect.h);
return {
.top = std::max(0.0f, static_cast<float>(safeRect.y)) * scaleY,
.right = std::max(0.0f, static_cast<float>(windowSize.width) - safeRight) * scaleX,
.bottom = std::max(0.0f, static_cast<float>(windowSize.height) - safeBottom) * scaleY,
.left = std::max(0.0f, static_cast<float>(safeRect.x)) * scaleX,
};
}
} // namespace
Window::Window() {
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
mDocument = context->LoadDocument("res/rml/window.rml");
if (mDocument == nullptr) {
return;
}
mKeyListener = std::make_unique<ScopedEventListener>(
mDocument, Rml::EventId::Keydown, [this](Rml::Event& event) {
// 1-9 for quick switching tabs
const auto key = static_cast<Rml::Input::KeyIdentifier>(
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
if (key >= Rml::Input::KeyIdentifier::KI_1 && key <= Rml::Input::KeyIdentifier::KI_9) {
if (set_active_tab(key - Rml::Input::KeyIdentifier::KI_1)) {
if (!mContentComponents.empty()) {
mContentComponents.front()->focus();
}
event.StopPropagation();
return;
Window::Window() : Document("res/rml/window.rml") {
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
// 1-9 for quick switching tabs
const auto key = static_cast<Rml::Input::KeyIdentifier>(
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
if (key >= Rml::Input::KeyIdentifier::KI_1 && key <= Rml::Input::KeyIdentifier::KI_9) {
if (set_active_tab(key - Rml::Input::KeyIdentifier::KI_1)) {
if (!mContentComponents.empty()) {
mContentComponents.front()->focus();
}
event.StopPropagation();
}
const auto cmd = map_nav_event(event);
if (cmd == NavCommand::None) {
return;
}
auto* target = event.GetTargetElement();
if (cmd == NavCommand::Next || cmd == NavCommand::Previous ||
target->Closest(".tab-bar")) {
if (handle_tab_bar_nav(event, cmd)) {
event.StopPropagation();
}
} else if (target->Closest(".content")) {
if (handle_content_nav(event, cmd)) {
event.StopPropagation();
}
}
});
}
Window::~Window() {
auto* context = aurora::rmlui::get_context();
if (context != nullptr && mDocument != nullptr) {
context->UnloadDocument(mDocument);
mDocument = nullptr;
}
}
void Window::show() {
if (mDocument != nullptr) {
mDocument->Show();
}
}
void Window::hide() {
if (mDocument != nullptr) {
mDocument->Hide();
}
}
void Window::focus_for_input() noexcept {
if (!mContentComponents.empty()) {
if (mContentComponents.front()->focus()) {
return;
}
}
focus_active_tab();
});
}
void Window::update() {
@@ -141,6 +47,7 @@ void Window::update() {
for (const auto& component : mContentComponents) {
component->update();
}
Document::update();
}
void Window::update_safe_area() noexcept {
@@ -208,9 +115,6 @@ void Window::add_tab(const Rml::String& title, TabBuilder builder) {
}),
.builder = std::move(builder),
});
if (index == mSelectedTabIndex) {
focus_active_tab();
}
}
void Window::clear_content() noexcept {
@@ -221,7 +125,7 @@ void Window::clear_content() noexcept {
}
}
bool Window::focus_active_tab() noexcept {
bool Window::focus() {
if (mTabs.empty()) {
return false;
}
@@ -232,6 +136,19 @@ bool Window::focus_active_tab() noexcept {
return mTabs[i].button->focus();
}
bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
auto* target = event.GetTargetElement();
if (cmd != NavCommand::Next && cmd != NavCommand::Previous && target->Closest(".content")) {
if (handle_content_nav(event, cmd)) {
return true;
}
}
if (handle_tab_bar_nav(event, cmd)) {
return true;
}
return false;
}
bool Window::handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept {
if (cmd == NavCommand::Down) {
if (!mContentComponents.empty()) {
@@ -277,7 +194,7 @@ bool Window::handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept {
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
if (cmd == NavCommand::Up || cmd == NavCommand::Cancel) {
return focus_active_tab();
return focus();
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
int currentComponent = -1;
for (int i = 0; i < mContentComponents.size(); ++i) {
+6 -24
View File
@@ -1,15 +1,14 @@
#pragma once
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/ElementDocument.h>
#include "button.hpp"
#include "component.hpp"
#include "document.hpp"
#include "ui.hpp"
#include "nav_types.hpp"
namespace dusk::ui {
class Window {
class Window : public Document {
public:
using TabBuilder = std::function<void(Rml::Element*)>;
struct Tab {
@@ -17,37 +16,21 @@ public:
std::unique_ptr<Button> button;
TabBuilder builder;
};
struct Insets {
float top = 0.0f;
float right = 0.0f;
float bottom = 0.0f;
float left = 0.0f;
bool operator==(const Insets& other) const noexcept {
return top == other.top && right == other.right && bottom == other.bottom &&
left == other.left;
}
};
Window();
~Window();
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
void show();
void hide();
void focus_for_input() noexcept;
void update();
void update() override;
bool focus() override;
bool set_active_tab(int index);
protected:
void add_tab(const Rml::String& title, TabBuilder builder);
void update_safe_area() noexcept;
void clear_content() noexcept;
bool focus_active_tab() noexcept;
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
bool handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept;
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
@@ -59,7 +42,6 @@ protected:
return ref;
}
Rml::ElementDocument* mDocument = nullptr;
std::vector<Tab> mTabs;
std::vector<std::unique_ptr<Component> > mContentComponents;
Insets mBodyPadding;
+3 -3
View File
@@ -589,11 +589,11 @@ int game_main(int argc, char* argv[]) {
dusk::ui::initialize();
// TODO: just for testing
auto& editorWindow = dusk::ui::add_window(std::make_unique<dusk::ui::EditorWindow>());
// auto& editorWindow = dusk::ui::add_document(std::make_unique<dusk::ui::EditorWindow>());
// editorWindow.show();
auto& settingsWindow = dusk::ui::add_window(std::make_unique<dusk::ui::SettingsWindow>());
// auto& settingsWindow = dusk::ui::add_document(std::make_unique<dusk::ui::SettingsWindow>());
// settingsWindow.show();
auto& popup = dusk::ui::add_popup(std::make_unique<dusk::ui::Popup>(settingsWindow, editorWindow));
auto& popup = dusk::ui::add_document(std::make_unique<dusk::ui::Popup>());
popup.show();
std::string dvd_path;