mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-09 04:30:49 -04:00
Begin scaffolding keyboard nav
This commit is contained in:
+16
-8
@@ -13,6 +13,11 @@ body {
|
||||
color: #E0DBC8;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
.window {
|
||||
max-width: 1088dp;
|
||||
max-height: 768dp;
|
||||
@@ -40,22 +45,25 @@ body {
|
||||
padding: 0 24dp;
|
||||
line-height: 64dp;
|
||||
opacity: 0.25;
|
||||
tab-index: auto;
|
||||
nav: horizontal;
|
||||
focus: auto;
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d00);
|
||||
transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
.window .tab-bar .tab.selected {
|
||||
opacity: 1;
|
||||
border-bottom: 4dp #C2A42D;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
decorator: linear-gradient(to bottom, rgba(194, 164, 45, 0%) 0%, rgba(194, 164, 45, 15%) 100%);
|
||||
}
|
||||
|
||||
.window .tab-bar .tab:focus-visible {
|
||||
.window .tab-bar .tab:focus-visible,
|
||||
.window .tab-bar .tab:hover {
|
||||
opacity: 1;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
decorator: linear-gradient(to bottom, rgba(194, 164, 45, 0%) 0%, rgba(194, 164, 45, 15%) 100%);
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d26);
|
||||
}
|
||||
|
||||
.window .tab-bar .tab:active {
|
||||
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
|
||||
}
|
||||
|
||||
.window .content {
|
||||
@@ -112,7 +120,7 @@ body {
|
||||
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
.button.active, .button:hover {
|
||||
.button.active, .button:hover, .button:focus-visible {
|
||||
background-color: rgba(204, 184, 119, 20%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
@@ -135,7 +143,7 @@ body {
|
||||
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
.select-button.active, .select-button:hover {
|
||||
.select-button.active, .select-button:hover, .select-button:focus-visible {
|
||||
background-color: rgba(204, 184, 119, 20%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ Button::Button(Rml::Element* parent, ButtonProps props, const Rml::String& class
|
||||
mProps.onPressed(event);
|
||||
}
|
||||
});
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
if (mProps.onPressed) {
|
||||
mProps.onPressed(event);
|
||||
}
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Button::set_text(const Rml::String& text) {
|
||||
|
||||
@@ -15,6 +15,20 @@ void Component::update() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Component::focus() {
|
||||
// Can we focus self?
|
||||
if (mRoot->Focus(true)) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, try to focus a child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
@@ -35,6 +49,15 @@ void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
bool Component::contains(Rml::Element* element) const {
|
||||
for (const auto* node = element; node != nullptr; node = node->GetParentNode()) {
|
||||
if (node == mRoot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Component::clear_children() {
|
||||
mChildren.clear();
|
||||
while (mRoot->GetNumChildren() > 0) {
|
||||
|
||||
@@ -23,15 +23,11 @@ public:
|
||||
Component& operator=(const Component&) = delete;
|
||||
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
void clear_children();
|
||||
bool contains(Rml::Element* element) const;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
@@ -41,6 +37,12 @@ protected:
|
||||
return ref;
|
||||
}
|
||||
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
void clear_children();
|
||||
|
||||
Rml::Element* mRoot = nullptr;
|
||||
std::vector<std::unique_ptr<Component> > mChildren;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
|
||||
@@ -173,7 +173,7 @@ bool handle_editor_action(const Rml::VariantList& arguments) {
|
||||
|
||||
EditorWindow::EditorWindow() {
|
||||
add_tab("Player Status", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content);
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
leftPane.add_section("Player");
|
||||
leftPane.add_select_button({
|
||||
.key = "Player Name",
|
||||
@@ -207,7 +207,7 @@ EditorWindow::EditorWindow() {
|
||||
.selected = false,
|
||||
});
|
||||
|
||||
auto& rightPane = add_child<Pane>(content);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
rightPane.add_button({
|
||||
.text = "Hello, world!",
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ ScopedEventListener::ScopedEventListener(
|
||||
ScopedEventListener::~ScopedEventListener() {
|
||||
if (mElement != nullptr) {
|
||||
mElement->RemoveEventListener(mEvent, this, mCapture);
|
||||
mElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ public:
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false);
|
||||
~ScopedEventListener() override;
|
||||
|
||||
ScopedEventListener(const ScopedEventListener&) = delete;
|
||||
ScopedEventListener& operator=(const ScopedEventListener&) = delete;
|
||||
ScopedEventListener(ScopedEventListener&&) = delete;
|
||||
ScopedEventListener& operator=(ScopedEventListener&&) = delete;
|
||||
|
||||
void ProcessEvent(Rml::Event& event) override;
|
||||
void OnDetach(Rml::Element* element) override;
|
||||
|
||||
|
||||
+54
-3
@@ -5,16 +5,67 @@
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
Rml::Element* createRoot(Rml::Element* parent, const Rml::String& className) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("div");
|
||||
elem->SetClass("pane", true);
|
||||
elem->SetClass(className, true);
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Pane::Pane(Rml::Element* parent) : Component(createRoot(parent)) {}
|
||||
Pane::Pane(Rml::Element* parent, Direction direction, const Rml::String& className)
|
||||
: Component(createRoot(parent, className)), mDirection(direction) {
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
int direction = 0;
|
||||
if ((mDirection == Direction::Vertical && cmd == NavCommand::Down) ||
|
||||
(mDirection == Direction::Horizontal && cmd == NavCommand::Right))
|
||||
{
|
||||
direction = 1;
|
||||
} else if ((mDirection == Direction::Vertical && cmd == NavCommand::Up) ||
|
||||
(mDirection == Direction::Horizontal && cmd == NavCommand::Left))
|
||||
{
|
||||
direction = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedChild = -1;
|
||||
for (size_t i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->contains(target)) {
|
||||
focusedChild = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int i = focusedChild + direction;
|
||||
if (focusedChild == -1) {
|
||||
// If the container itself is focused and next is pressed, focus the first element
|
||||
if (direction == 1) {
|
||||
i = 0;
|
||||
} else {
|
||||
// Otherwise, allow event to bubble
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (i >= 0 && i < static_cast<int>(mChildren.size())) {
|
||||
if (mChildren[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
break;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool Pane::focus() {
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
|
||||
+11
-1
@@ -8,7 +8,14 @@ namespace dusk::ui {
|
||||
|
||||
class Pane : public Component {
|
||||
public:
|
||||
explicit Pane(Rml::Element* parent);
|
||||
enum class Direction {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
};
|
||||
|
||||
explicit Pane(Rml::Element* parent, Direction direction, const Rml::String& className = "pane");
|
||||
|
||||
bool focus() override;
|
||||
|
||||
Rml::Element* add_section(const Rml::String& text);
|
||||
Button& add_button(Button::Props props) { return add_child<Button>(mRoot, std::move(props)); }
|
||||
@@ -17,6 +24,9 @@ public:
|
||||
}
|
||||
Rml::Element* add_text(const Rml::String& text);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
Direction mDirection;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+107
-58
@@ -6,13 +6,73 @@
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
template <typename T>
|
||||
struct ConfigProps {
|
||||
Rml::String key;
|
||||
ConfigVar<T>* value;
|
||||
std::function<void(T)> onChange;
|
||||
Rml::String helpText;
|
||||
Pane* rightPane = nullptr;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
class ConfigBoolSelect : public SelectButton {
|
||||
public:
|
||||
using Props = ConfigProps<bool>;
|
||||
|
||||
ConfigBoolSelect(Rml::Element* parent, Props props)
|
||||
: SelectButton(
|
||||
parent, {
|
||||
.key = std::move(props.key),
|
||||
.getValue = [this] { return mValue->getValue() ? "On" : "Off"; },
|
||||
.onPressed = [this](SelectButton&, Rml::Event&) { toggle_value(); },
|
||||
.selected = props.selected,
|
||||
}),
|
||||
mValue(props.value), onChange(std::move(props.onChange)),
|
||||
mHelpText(std::move(props.helpText)), mRightPane(props.rightPane) {
|
||||
if (!mHelpText.empty() && mRightPane != nullptr) {
|
||||
listen(nullptr, Rml::EventId::Focus, [this](Rml::Event&) {
|
||||
mRightPane->clear();
|
||||
mRightPane->add_text(mHelpText);
|
||||
});
|
||||
listen(nullptr, Rml::EventId::Mouseover, [this](Rml::Event&) {
|
||||
mRightPane->clear();
|
||||
mRightPane->add_text(mHelpText);
|
||||
});
|
||||
}
|
||||
listen(nullptr, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::Left || cmd == NavCommand::Right || cmd == NavCommand::Confirm) {
|
||||
toggle_value();
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
void toggle_value() {
|
||||
const bool newValue = !mValue->getValue();
|
||||
mValue->setValue(newValue);
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
}
|
||||
config::Save();
|
||||
}
|
||||
|
||||
ConfigVar<bool>* mValue;
|
||||
std::function<void(bool)> onChange;
|
||||
Rml::String mHelpText;
|
||||
Pane* mRightPane;
|
||||
};
|
||||
|
||||
SettingsWindow::SettingsWindow() {
|
||||
add_tab("Audio", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content);
|
||||
auto& rightPane = add_child<Pane>(content);
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
|
||||
leftPane.add_section("Volume");
|
||||
{
|
||||
@@ -32,37 +92,30 @@ SettingsWindow::SettingsWindow() {
|
||||
}
|
||||
|
||||
leftPane.add_section("Effects");
|
||||
{
|
||||
auto& btn = leftPane.add_select_button({
|
||||
.key = "Enable Reverb",
|
||||
.getValue = [] { return getSettings().audio.enableReverb ? "On" : "Off"; },
|
||||
.onPressed =
|
||||
[](SelectButton& self, Rml::Event& event) {
|
||||
getSettings().audio.enableReverb.setValue(
|
||||
!getSettings().audio.enableReverb);
|
||||
dusk::audio::SetEnableReverb(getSettings().audio.enableReverb);
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
btn.listen(nullptr, Rml::EventId::Focus, [&](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Enables the reverb effect in game audio.");
|
||||
});
|
||||
btn.listen(nullptr, Rml::EventId::Mouseover, [&](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Enables the reverb effect in game audio.");
|
||||
});
|
||||
}
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = "Enable Reverb",
|
||||
.value = &getSettings().audio.enableReverb,
|
||||
.onChange = [](bool value) { audio::SetEnableReverb(value); },
|
||||
.helpText = "Enables the reverb effect in game audio.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
|
||||
leftPane.add_section("Tweaks");
|
||||
leftPane.add_select_button({
|
||||
.key = "No Low HP Sound",
|
||||
.value = "Off",
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Non-Stop Midna's Lament",
|
||||
.value = "On",
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = "No Low HP Sound",
|
||||
.value = &getSettings().game.noLowHpSound,
|
||||
.helpText = "Disable the beeping sound when having low health.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(leftPane.root(),
|
||||
ConfigBoolSelect::Props{
|
||||
.key = "Non-Stop Midna's Lament",
|
||||
.value = &getSettings().game.midnasLamentNonStop,
|
||||
.helpText = "Prevents enemy music while Midna's Lament is playing.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Cheats", [this](Rml::Element* content) {
|
||||
@@ -74,10 +127,11 @@ SettingsWindow::SettingsWindow() {
|
||||
});
|
||||
|
||||
add_tab("Graphics", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content);
|
||||
auto& rightPane = add_child<Pane>(content);
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
|
||||
|
||||
leftPane.add_section("Display");
|
||||
|
||||
leftPane.add_button({
|
||||
.text = "Toggle Fullscreen",
|
||||
.onPressed =
|
||||
@@ -98,31 +152,26 @@ SettingsWindow::SettingsWindow() {
|
||||
VICenterWindow();
|
||||
},
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Enable VSync",
|
||||
.getValue = [] { return getSettings().video.enableVsync ? "On" : "Off"; },
|
||||
.onPressed =
|
||||
[](SelectButton&, Rml::Event&) {
|
||||
getSettings().video.enableVsync.setValue(!getSettings().video.enableVsync);
|
||||
aurora_enable_vsync(getSettings().video.enableVsync);
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Force 4:3 Aspect Ratio",
|
||||
.getValue = [] { return getSettings().video.lockAspectRatio ? "On" : "Off"; },
|
||||
.onPressed =
|
||||
[](SelectButton&, Rml::Event&) {
|
||||
getSettings().video.lockAspectRatio.setValue(
|
||||
!getSettings().video.lockAspectRatio);
|
||||
if (getSettings().video.lockAspectRatio) {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
|
||||
} else {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH);
|
||||
}
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(leftPane.root(),
|
||||
ConfigBoolSelect::Props{
|
||||
.key = "Enable VSync",
|
||||
.value = &getSettings().video.enableVsync,
|
||||
.onChange = [](bool value) { aurora_enable_vsync(value); },
|
||||
.helpText = "Synchronizes the frame rate to your monitor's refresh rate.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
leftPane.add_child<ConfigBoolSelect>(
|
||||
leftPane.root(), ConfigBoolSelect::Props{
|
||||
.key = "Lock 4:3 Aspect Ratio",
|
||||
.value = &getSettings().video.lockAspectRatio,
|
||||
.onChange =
|
||||
[](bool value) {
|
||||
AuroraSetViewportPolicy(
|
||||
value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH);
|
||||
},
|
||||
.helpText = "Lock the game's aspect ratio to the original.",
|
||||
.rightPane = &rightPane,
|
||||
});
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
});
|
||||
|
||||
@@ -8,4 +8,4 @@ public:
|
||||
SettingsWindow();
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace dusk::ui
|
||||
@@ -97,4 +97,29 @@ std::string escape(std::string_view str) noexcept {
|
||||
return result;
|
||||
}
|
||||
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept {
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
switch (key) {
|
||||
case Rml::Input::KeyIdentifier::KI_UP:
|
||||
return NavCommand::Up;
|
||||
case Rml::Input::KeyIdentifier::KI_DOWN:
|
||||
return NavCommand::Down;
|
||||
case Rml::Input::KeyIdentifier::KI_LEFT:
|
||||
return NavCommand::Left;
|
||||
case Rml::Input::KeyIdentifier::KI_RIGHT:
|
||||
return NavCommand::Right;
|
||||
case Rml::Input::KeyIdentifier::KI_ESCAPE:
|
||||
return NavCommand::Cancel;
|
||||
case Rml::Input::KeyIdentifier::KI_RETURN:
|
||||
return NavCommand::Confirm;
|
||||
case Rml::Input::KeyIdentifier::KI_NEXT:
|
||||
return NavCommand::Next;
|
||||
case Rml::Input::KeyIdentifier::KI_PRIOR:
|
||||
return NavCommand::Previous;
|
||||
default:
|
||||
return NavCommand::None;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+15
-1
@@ -1,12 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <RmlUi/Core/Event.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace dusk::ui {
|
||||
class Window;
|
||||
|
||||
enum class NavCommand {
|
||||
None,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Next, // R1
|
||||
Previous, // L1
|
||||
Confirm, // A
|
||||
Cancel, // B
|
||||
};
|
||||
|
||||
bool initialize() noexcept;
|
||||
void shutdown() noexcept;
|
||||
|
||||
@@ -19,4 +31,6 @@ void remove_window(Window& window) noexcept;
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
||||
std::string escape(std::string_view str) noexcept;
|
||||
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+126
-9
@@ -2,10 +2,14 @@
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "button.hpp"
|
||||
#include "magic_enum.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
static aurora::Module Log{"dusk::ui::window"};
|
||||
|
||||
Window::Window() {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
@@ -17,6 +21,37 @@ Window::Window() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::None) {
|
||||
return;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
if (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() {
|
||||
@@ -45,19 +80,23 @@ void Window::update() {
|
||||
}
|
||||
}
|
||||
|
||||
void Window::set_active_tab(int index) {
|
||||
bool Window::set_active_tab(int index) {
|
||||
if (index < 0 || index >= mTabs.size() || index == mSelectedTabIndex) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
clear_content();
|
||||
for (int i = 0; i < mTabs.size(); i++) {
|
||||
mTabs[i].button->set_selected(i == index);
|
||||
}
|
||||
mSelectedTabIndex = index;
|
||||
const auto& tab = mTabs[index];
|
||||
if (tab.builder) {
|
||||
tab.builder(mDocument->GetElementById("content"));
|
||||
if (tab.button->focus()) {
|
||||
clear_content();
|
||||
for (int i = 0; i < mTabs.size(); i++) {
|
||||
mTabs[i].button->set_selected(i == index);
|
||||
}
|
||||
mSelectedTabIndex = index;
|
||||
if (tab.builder) {
|
||||
tab.builder(mDocument->GetElementById("content"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Window::add_tab(const Rml::String& title, TabBuilder builder) {
|
||||
@@ -87,4 +126,82 @@ void Window::clear_content() noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::focus_active_tab() noexcept {
|
||||
if (mTabs.empty()) {
|
||||
return false;
|
||||
}
|
||||
int i = mSelectedTabIndex;
|
||||
if (i < 0 || i >= mTabs.size()) {
|
||||
i = 0;
|
||||
}
|
||||
return mTabs[i].button->focus();
|
||||
}
|
||||
|
||||
bool Window::handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Down) {
|
||||
if (!mContentComponents.empty()) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
int currentComponent = -1;
|
||||
for (int i = 0; i < mTabs.size(); ++i) {
|
||||
if (mTabs[i].button->contains(event.GetTargetElement())) {
|
||||
currentComponent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int direction = cmd == NavCommand::Right ? 1 : -1;
|
||||
int i = currentComponent + direction;
|
||||
if (currentComponent == -1) {
|
||||
// If the container itself is focused and right is pressed, focus the first element
|
||||
if (cmd == NavCommand::Right) {
|
||||
i = 0;
|
||||
} else {
|
||||
// Otherwise, allow event to bubble
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while (i >= 0 && i < static_cast<int>(mTabs.size())) {
|
||||
if (set_active_tab(i)) {
|
||||
return true;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
// TODO: close window
|
||||
} else if (cmd == NavCommand::Confirm) {
|
||||
if (!mContentComponents.empty()) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Up || cmd == NavCommand::Cancel) {
|
||||
return focus_active_tab();
|
||||
} else if (cmd == NavCommand::Down) {
|
||||
if (!mContentComponents.empty()) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
int currentComponent = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); ++i) {
|
||||
if (mContentComponents[i]->contains(event.GetTargetElement())) {
|
||||
currentComponent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int direction = cmd == NavCommand::Right ? 1 : -1;
|
||||
int i = currentComponent + direction;
|
||||
if (currentComponent == -1) {
|
||||
if (cmd == NavCommand::Right) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
} else if (i >= 0 && i < mContentComponents.size()) {
|
||||
return mContentComponents[i]->focus();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
@@ -27,11 +28,14 @@ public:
|
||||
void hide();
|
||||
|
||||
void update();
|
||||
void set_active_tab(int index);
|
||||
bool set_active_tab(int index);
|
||||
|
||||
protected:
|
||||
void add_tab(const Rml::String& title, TabBuilder builder);
|
||||
void clear_content() noexcept;
|
||||
bool focus_active_tab() noexcept;
|
||||
bool handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept;
|
||||
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
@@ -45,6 +49,7 @@ protected:
|
||||
std::vector<Tab> mTabs;
|
||||
std::vector<std::unique_ptr<Component> > mContentComponents;
|
||||
int mSelectedTabIndex = 0;
|
||||
std::unique_ptr<ScopedEventListener> mKeyListener;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
Reference in New Issue
Block a user