mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-03 16:31:56 -04:00
Dual pane navigation & more player editor
This commit is contained in:
+10
-11
@@ -16,7 +16,7 @@ Rml::Element* createRoot(Rml::Element* parent, const Rml::String& tagName) {
|
||||
} // namespace
|
||||
|
||||
Button::Button(Rml::Element* parent, Props props, const Rml::String& tagName)
|
||||
: Component(createRoot(parent, tagName)) {
|
||||
: FluentComponent(createRoot(parent, tagName)) {
|
||||
update_props(std::move(props));
|
||||
}
|
||||
|
||||
@@ -27,19 +27,12 @@ void Button::set_text(const Rml::String& text) {
|
||||
}
|
||||
}
|
||||
|
||||
void Button::set_selected(bool selected) {
|
||||
if (mProps.selected != selected) {
|
||||
mRoot->SetPseudoClass("selected", selected);
|
||||
mProps.selected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
Button& Button::on_pressed(ButtonCallback callback) {
|
||||
if (!callback) {
|
||||
return *this;
|
||||
}
|
||||
listen(mRoot, Rml::EventId::Click, [callback](Rml::Event&) { callback(); });
|
||||
listen(mRoot, Rml::EventId::Keydown, [callback = std::move(callback)](Rml::Event& event) {
|
||||
listen(Rml::EventId::Click, [callback](Rml::Event&) { callback(); });
|
||||
listen(Rml::EventId::Keydown, [callback = std::move(callback)](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
callback();
|
||||
@@ -51,7 +44,6 @@ Button& Button::on_pressed(ButtonCallback callback) {
|
||||
|
||||
void Button::update_props(Props props) {
|
||||
set_text(props.text);
|
||||
set_selected(props.selected);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
@@ -62,4 +54,11 @@ void ControlledButton::update() {
|
||||
Button::update();
|
||||
}
|
||||
|
||||
bool ControlledButton::selected() const {
|
||||
if (mIsSelected) {
|
||||
return mIsSelected();
|
||||
}
|
||||
return Button::selected();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -6,11 +6,10 @@ namespace dusk::ui {
|
||||
|
||||
using ButtonCallback = std::function<void()>;
|
||||
|
||||
class Button : public Component {
|
||||
class Button : public FluentComponent<Button> {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
Button(Rml::Element* parent, Props props, const Rml::String& tagName = "button");
|
||||
@@ -18,7 +17,6 @@ public:
|
||||
: Button(parent, Props{std::move(text)}, tagName) {}
|
||||
|
||||
void set_text(const Rml::String& text);
|
||||
void set_selected(bool selected);
|
||||
Button& on_pressed(ButtonCallback callback);
|
||||
|
||||
const Rml::String& get_text() const { return mProps.text; }
|
||||
@@ -37,10 +35,11 @@ public:
|
||||
};
|
||||
|
||||
ControlledButton(Rml::Element* parent, Props props, const Rml::String& tagName = "button")
|
||||
: Button(parent, Button::Props{std::move(props.text)}, tagName),
|
||||
: Button(parent, {std::move(props.text)}, tagName),
|
||||
mIsSelected(std::move(props.isSelected)) {}
|
||||
|
||||
void update() override;
|
||||
bool selected() const override;
|
||||
|
||||
private:
|
||||
std::function<bool()> mIsSelected;
|
||||
|
||||
@@ -13,6 +13,9 @@ void Component::update() {
|
||||
}
|
||||
|
||||
bool Component::focus() {
|
||||
if (disabled()) {
|
||||
return false;
|
||||
}
|
||||
// Can we focus self?
|
||||
if (mRoot->Focus(true)) {
|
||||
mRoot->ScrollIntoView(Rml::ScrollIntoViewOptions{
|
||||
@@ -32,6 +35,31 @@ bool Component::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void Component::set_selected(bool value) {
|
||||
// Subclasses may override selected() to return a dynamic value, but
|
||||
// we're only interested in if the pseudoclass is set or not, so we
|
||||
// use Component::selected() directly rather than selected().
|
||||
if (Component::selected() == value) {
|
||||
return;
|
||||
}
|
||||
mRoot->SetPseudoClass("selected", value);
|
||||
mRoot->DispatchEvent(Rml::EventId::Change, {{"selected", Rml::Variant{value}}});
|
||||
}
|
||||
|
||||
void Component::set_disabled(bool value) {
|
||||
if (Component::disabled() == value) {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
mRoot->SetAttribute("disabled", "");
|
||||
mRoot->SetPseudoClass("disabled", true);
|
||||
mRoot->Blur();
|
||||
} else {
|
||||
mRoot->RemoveAttribute("disabled");
|
||||
mRoot->SetPseudoClass("disabled", false);
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
@@ -42,7 +70,6 @@ Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
}
|
||||
return parent->AppendChild(doc->CreateElement(tag));
|
||||
}
|
||||
|
||||
void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
|
||||
@@ -26,15 +26,13 @@ public:
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
|
||||
virtual bool selected() const { return mRoot->IsPseudoClassSet("selected"); }
|
||||
virtual void set_selected(bool selected);
|
||||
virtual bool disabled() const { return mRoot->IsPseudoClassSet("disabled"); }
|
||||
virtual void set_disabled(bool disabled);
|
||||
|
||||
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(mRoot, event, std::move(callback), capture);
|
||||
}
|
||||
void on_hover(ScopedEventListener::Callback callback) {
|
||||
listen(Rml::EventId::Mouseover, callback);
|
||||
listen(Rml::EventId::Focus, std::move(callback));
|
||||
}
|
||||
bool contains(Rml::Element* element) const;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
@@ -46,7 +44,6 @@ public:
|
||||
}
|
||||
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
bool selected() const { return mRoot->IsPseudoClassSet("selected"); }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
@@ -57,4 +54,27 @@ protected:
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
};
|
||||
|
||||
template <class Derived>
|
||||
class FluentComponent : public Component {
|
||||
public:
|
||||
using Component::Component;
|
||||
|
||||
Derived& listen(
|
||||
Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) {
|
||||
Component::listen(mRoot, event, std::move(callback), capture);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
Derived& on_hover(ScopedEventListener::Callback callback) {
|
||||
listen(Rml::EventId::Mouseover, callback);
|
||||
listen(Rml::EventId::Focus, std::move(callback));
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
Derived& on_focus(ScopedEventListener::Callback callback) {
|
||||
listen(Rml::EventId::Focus, std::move(callback));
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+206
-12
@@ -5,12 +5,16 @@
|
||||
|
||||
#include "button.hpp"
|
||||
#include "d/actor/d_a_player.h"
|
||||
#include "d/d_kankyo.h"
|
||||
#include "d/d_meter2_info.h"
|
||||
#include "number_button.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "select_button.hpp"
|
||||
#include "string_button.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -25,6 +29,13 @@ dSv_player_status_a_c* get_player_status() {
|
||||
return &dComIfGs_getSaveData()->getPlayer().getPlayerStatusA();
|
||||
}
|
||||
|
||||
dSv_player_status_b_c* get_player_status_b() {
|
||||
if (!has_save_data()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &dComIfGs_getSaveData()->getPlayer().getPlayerStatusB();
|
||||
}
|
||||
|
||||
Rml::String get_player_name() {
|
||||
if (!has_save_data()) {
|
||||
return "";
|
||||
@@ -328,6 +339,31 @@ Rml::String item_label_for_slot(u8 slot) {
|
||||
return fmt::format("Slot {0} ({1})", slot, get_item_name(id));
|
||||
}
|
||||
|
||||
constexpr std::array<Rml::String, 3> walletSizeNames = {
|
||||
"Normal",
|
||||
"Big",
|
||||
"Giant",
|
||||
};
|
||||
|
||||
constexpr std::array<Rml::String, 2> formNames = {
|
||||
"Human",
|
||||
"Wolf",
|
||||
};
|
||||
|
||||
constexpr float kDaytimeUnitsPerHour = 15.0f;
|
||||
|
||||
float daytime_from_clock(int hour, int minute) {
|
||||
hour = std::clamp(hour, 0, 23);
|
||||
minute = std::clamp(minute, 0, 59);
|
||||
return (hour * kDaytimeUnitsPerHour) + (minute / 60.0f * kDaytimeUnitsPerHour);
|
||||
}
|
||||
|
||||
void set_clock_time(int hour, int minute) {
|
||||
if (auto* statusB = get_player_status_b()) {
|
||||
statusB->setTime(daytime_from_clock(hour, minute));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorWindow::EditorWindow() {
|
||||
@@ -343,7 +379,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = set_player_name,
|
||||
.maxLength = 16,
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<StringButton>(StringButton::Props{
|
||||
.key = "Horse Name",
|
||||
@@ -351,8 +387,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = set_horse_name,
|
||||
.maxLength = 16,
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
;
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Max Health",
|
||||
@@ -360,7 +395,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = [](int value) { return get_player_status()->setMaxLife(value); },
|
||||
.max = UINT16_MAX, // TODO: actual max
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Health",
|
||||
@@ -368,7 +403,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = [](int value) { return get_player_status()->setLife(value); },
|
||||
.max = UINT16_MAX, // TODO: actual max
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Rupees",
|
||||
@@ -376,7 +411,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = [](int value) { return get_player_status()->setRupee(value); },
|
||||
.max = get_player_status()->getRupeeMax(),
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Max Oil",
|
||||
@@ -384,7 +419,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = [](int value) { return get_player_status()->setMaxOil(value); },
|
||||
.max = UINT16_MAX, // TODO: actual max
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Oil",
|
||||
@@ -392,7 +427,7 @@ EditorWindow::EditorWindow() {
|
||||
.setValue = [](int value) { return get_player_status()->setOil(value); },
|
||||
.max = UINT16_MAX, // TODO: actual max
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
|
||||
leftPane.add_section("Equipment");
|
||||
const auto genSelectItemComboBox = [&leftPane, &rightPane](
|
||||
@@ -402,14 +437,15 @@ EditorWindow::EditorWindow() {
|
||||
.key = label,
|
||||
.getValue = [&selectItemData] { return item_label_for_slot(selectItemData); },
|
||||
})
|
||||
.on_hover([&rightPane, &selectItemData](Rml::Event&) {
|
||||
.on_focus([&rightPane, &selectItemData](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_button(
|
||||
{
|
||||
.text = "None",
|
||||
.isSelected = [&selectItemData] { return selectItemData == 0xFF; },
|
||||
.isSelected =
|
||||
[&selectItemData] { return selectItemData == dItemNo_NONE_e; },
|
||||
},
|
||||
[&selectItemData] { selectItemData = 0xFF; });
|
||||
[&selectItemData] { selectItemData = dItemNo_NONE_e; });
|
||||
for (int i = 0; i < 24; i++) {
|
||||
rightPane.add_button(
|
||||
{
|
||||
@@ -430,7 +466,7 @@ EditorWindow::EditorWindow() {
|
||||
.key = "Clothes",
|
||||
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[0]); },
|
||||
})
|
||||
.on_hover([&rightPane](Rml::Event&) {
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
const auto addOption = [&rightPane](u8 id) {
|
||||
rightPane.add_button(
|
||||
@@ -449,6 +485,164 @@ EditorWindow::EditorWindow() {
|
||||
addOption(dItemNo_WEAR_ZORA_e);
|
||||
addOption(dItemNo_ARMOR_e);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Sword",
|
||||
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[1]); },
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
const auto addOption = [&rightPane](u8 id) {
|
||||
rightPane.add_button(
|
||||
{
|
||||
.text = get_item_name(id),
|
||||
.isSelected =
|
||||
[id] { return get_player_status()->mSelectEquip[1] == id; },
|
||||
},
|
||||
[id] { get_player_status()->mSelectEquip[1] = id; });
|
||||
};
|
||||
addOption(dItemNo_NONE_e);
|
||||
addOption(dItemNo_WOOD_STICK_e);
|
||||
addOption(dItemNo_SWORD_e);
|
||||
addOption(dItemNo_MASTER_SWORD_e);
|
||||
addOption(dItemNo_LIGHT_SWORD_e);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Shield",
|
||||
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[2]); },
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
const auto addOption = [&rightPane](u8 id) {
|
||||
rightPane.add_button(
|
||||
{
|
||||
.text = get_item_name(id),
|
||||
.isSelected =
|
||||
[id] { return get_player_status()->mSelectEquip[2] == id; },
|
||||
},
|
||||
[id] { get_player_status()->mSelectEquip[2] = id; });
|
||||
};
|
||||
addOption(dItemNo_NONE_e);
|
||||
addOption(dItemNo_SHIELD_e);
|
||||
addOption(dItemNo_WOOD_SHIELD_e);
|
||||
addOption(dItemNo_HYLIA_SHIELD_e);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Scent",
|
||||
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[3]); },
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
const auto addOption = [&rightPane](u8 id) {
|
||||
rightPane.add_button(
|
||||
{
|
||||
.text = get_item_name(id),
|
||||
.isSelected =
|
||||
[id] { return get_player_status()->mSelectEquip[3] == id; },
|
||||
},
|
||||
[id] { get_player_status()->mSelectEquip[3] = id; });
|
||||
};
|
||||
addOption(dItemNo_NONE_e);
|
||||
addOption(dItemNo_SMELL_CHILDREN_e);
|
||||
addOption(dItemNo_SMELL_YELIA_POUCH_e);
|
||||
addOption(dItemNo_SMELL_POH_e);
|
||||
addOption(dItemNo_SMELL_FISH_e);
|
||||
addOption(dItemNo_SMELL_MEDICINE_e);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Wallet Size",
|
||||
.getValue = [] { return walletSizeNames[get_player_status()->getWalletSize()]; },
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
for (int i = 0; i < walletSizeNames.size(); ++i) {
|
||||
rightPane.add_button(
|
||||
{
|
||||
.text = walletSizeNames[i],
|
||||
.isSelected = [i] { return get_player_status()->getWalletSize() == i; },
|
||||
},
|
||||
[i] { get_player_status()->setWalletSize(i); });
|
||||
}
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Form",
|
||||
.getValue = [] { return formNames[get_player_status()->getTransformStatus()]; },
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
for (int i = 0; i < formNames.size(); ++i) {
|
||||
rightPane.add_button(
|
||||
{
|
||||
.text = formNames[i],
|
||||
.isSelected =
|
||||
[i] { return get_player_status()->getTransformStatus() == i; },
|
||||
},
|
||||
[i] { get_player_status()->setTransformStatus(i); });
|
||||
}
|
||||
});
|
||||
|
||||
leftPane.add_section("World");
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Day",
|
||||
.getValue = [] { return get_player_status_b()->getDate(); },
|
||||
.setValue =
|
||||
[](int value) { get_player_status_b()->setDate(static_cast<u16>(value)); },
|
||||
.max = UINT16_MAX,
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Hour",
|
||||
.getValue = [] { return dKy_getdaytime_hour(); },
|
||||
.setValue = [](int value) { set_clock_time(value, dKy_getdaytime_minute()); },
|
||||
.max = 23,
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Minute",
|
||||
.getValue = [] { return dKy_getdaytime_minute(); },
|
||||
.setValue = [](int value) { set_clock_time(dKy_getdaytime_hour(), value); },
|
||||
.max = 59,
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Transform Level",
|
||||
.getValue =
|
||||
[] {
|
||||
return std::popcount(static_cast<unsigned>(
|
||||
get_player_status_b()->mTransformLevelFlag & 0x7));
|
||||
},
|
||||
.setValue =
|
||||
[](int value) {
|
||||
get_player_status_b()->mTransformLevelFlag =
|
||||
static_cast<u8>((1u << value) - 1u);
|
||||
},
|
||||
.max = 3,
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Twilight Clear Level",
|
||||
.getValue =
|
||||
[] {
|
||||
return std::popcount(static_cast<unsigned>(
|
||||
get_player_status_b()->mDarkClearLevelFlag & 0x7));
|
||||
},
|
||||
.setValue =
|
||||
[](int value) {
|
||||
get_player_status_b()->mDarkClearLevelFlag =
|
||||
static_cast<u8>((1u << value) - 1u);
|
||||
},
|
||||
.max = 3,
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
|
||||
});
|
||||
|
||||
add_tab("Location", [this](Rml::Element* content) {
|
||||
|
||||
+71
-7
@@ -14,9 +14,29 @@ Rml::Element* createRoot(Rml::Element* parent) {
|
||||
} // namespace
|
||||
|
||||
Pane::Pane(Rml::Element* parent, Direction direction)
|
||||
: Component(createRoot(parent)), mDirection(direction) {
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
: FluentComponent(createRoot(parent)), mDirection(direction) {
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
|
||||
// If
|
||||
if ((mDirection == Direction::Vertical && cmd == NavCommand::Right) ||
|
||||
(mDirection == Direction::Horizontal && cmd == NavCommand::Down))
|
||||
{
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedChild = -1;
|
||||
for (size_t i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->contains(target)) {
|
||||
focusedChild = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (focusedChild == -1) {
|
||||
return;
|
||||
}
|
||||
set_selected_item(focusedChild);
|
||||
return;
|
||||
}
|
||||
|
||||
int direction = 0;
|
||||
if ((mDirection == Direction::Vertical && cmd == NavCommand::Down) ||
|
||||
(mDirection == Direction::Horizontal && cmd == NavCommand::Right))
|
||||
@@ -41,7 +61,7 @@ Pane::Pane(Rml::Element* parent, Direction direction)
|
||||
return;
|
||||
}
|
||||
int i = focusedChild + direction;
|
||||
while (i >= 0 && i < static_cast<int>(mChildren.size())) {
|
||||
while (i >= 0 && i < mChildren.size()) {
|
||||
if (mChildren[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
break;
|
||||
@@ -49,6 +69,30 @@ Pane::Pane(Rml::Element* parent, Direction direction)
|
||||
i += direction;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for selection change events
|
||||
listen(Rml::EventId::Change, [this](Rml::Event& event) {
|
||||
const auto it = std::find_if(event.GetParameters().begin(), event.GetParameters().end(),
|
||||
[](const auto& param) { return param.first == "selected"; });
|
||||
if (it != event.GetParameters().end()) {
|
||||
const auto selected = it->second.Get<bool>();
|
||||
int childIndex = -1;
|
||||
for (int i = 0; i < mChildren.size(); ++i) {
|
||||
if (event.GetTargetElement() == mChildren[i]->root()) {
|
||||
childIndex = i;
|
||||
}
|
||||
}
|
||||
if (childIndex != -1) {
|
||||
if (selected) {
|
||||
set_selected_item(childIndex);
|
||||
} else if (childIndex == mSelectedItem) {
|
||||
set_selected_item(-1);
|
||||
}
|
||||
} else {
|
||||
set_selected_item(-1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Pane::update() {
|
||||
@@ -56,13 +100,33 @@ void Pane::update() {
|
||||
Component::update();
|
||||
}
|
||||
|
||||
void Pane::set_selected_item(int index) {
|
||||
if (mSelectedItem == index) {
|
||||
return;
|
||||
}
|
||||
if (mSelectedItem >= 0 && mSelectedItem < mChildren.size()) {
|
||||
mChildren[mSelectedItem]->set_selected(false);
|
||||
}
|
||||
if (index >= 0 && index < mChildren.size()) {
|
||||
mSelectedItem = index;
|
||||
mChildren[index]->set_selected(true);
|
||||
} else {
|
||||
mSelectedItem = -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool Pane::focus() {
|
||||
// If there's a selected child, focus that
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->selected() && child->focus()) {
|
||||
return true;
|
||||
// Update selected child
|
||||
for (int i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->selected()) {
|
||||
mSelectedItem = i;
|
||||
}
|
||||
}
|
||||
// If there's a selected child, focus that
|
||||
if (mSelectedItem >= 0 && mSelectedItem < mChildren.size() && mChildren[mSelectedItem]->focus())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
|
||||
+16
-6
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Pane : public Component {
|
||||
class Pane : public FluentComponent<Pane> {
|
||||
public:
|
||||
enum class Direction {
|
||||
Vertical,
|
||||
@@ -18,13 +18,22 @@ public:
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
|
||||
void set_selected_item(int index);
|
||||
|
||||
Rml::Element* add_section(const Rml::String& text);
|
||||
ControlledButton& add_button(ControlledButton::Props props, ButtonCallback callback) {
|
||||
return static_cast<ControlledButton&>(
|
||||
add_child<ControlledButton>(std::move(props)).on_pressed(std::move(callback)));
|
||||
ControlledButton& add_button(ControlledButton::Props props, ButtonCallback callback = {}) {
|
||||
auto& btn = add_child<ControlledButton>(std::move(props));
|
||||
if (callback) {
|
||||
btn.on_pressed(std::move(callback));
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
Button& add_button(Rml::String text, ButtonCallback callback) {
|
||||
return add_child<Button>(std::move(text)).on_pressed(std::move(callback));
|
||||
Button& add_button(Rml::String text, ButtonCallback callback = {}) {
|
||||
auto& btn = add_child<Button>(std::move(text));
|
||||
if (callback) {
|
||||
btn.on_pressed(std::move(callback));
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
ControlledSelectButton& add_select_button(ControlledSelectButton::Props props) {
|
||||
return add_child<ControlledSelectButton>(std::move(props));
|
||||
@@ -37,6 +46,7 @@ public:
|
||||
private:
|
||||
Direction mDirection;
|
||||
bool finalized = false;
|
||||
int mSelectedItem = -1;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "dusk/main.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -35,13 +37,10 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
|
||||
});
|
||||
mTabBar->add_tab("Editor", [] { push_document(std::make_unique<EditorWindow>()); });
|
||||
mTabBar->add_tab("Reset", [this] {
|
||||
// TODO
|
||||
mTabBar->set_active_tab(-1);
|
||||
});
|
||||
mTabBar->add_tab("Exit", [this] {
|
||||
// TODO
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
mTabBar->set_active_tab(-1);
|
||||
});
|
||||
mTabBar->add_tab("Exit", [] { IsRunning = false; });
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
|
||||
@@ -15,20 +15,21 @@ Rml::Element* createRoot(Rml::Element* parent) {
|
||||
|
||||
} // namespace
|
||||
|
||||
SelectButton::SelectButton(Rml::Element* parent, Props props) : Component(createRoot(parent)) {
|
||||
SelectButton::SelectButton(Rml::Element* parent, Props props)
|
||||
: FluentComponent(createRoot(parent)) {
|
||||
mKeyElem = append(mRoot, "key");
|
||||
mValueElem = append(mRoot, "value");
|
||||
update_props(std::move(props));
|
||||
listen(mRoot, Rml::EventId::Click, [this](Rml::Event& event) {
|
||||
if (mProps.disabled) {
|
||||
listen(Rml::EventId::Click, [this](Rml::Event& event) {
|
||||
if (disabled()) {
|
||||
return;
|
||||
}
|
||||
if (handle_nav_command(NavCommand::Confirm)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
if (mProps.disabled) {
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
if (disabled()) {
|
||||
return;
|
||||
}
|
||||
const auto cmd = map_nav_event(event);
|
||||
@@ -38,34 +39,6 @@ SelectButton::SelectButton(Rml::Element* parent, Props props) : Component(create
|
||||
});
|
||||
}
|
||||
|
||||
bool SelectButton::focus() {
|
||||
if (mProps.disabled) {
|
||||
return false;
|
||||
}
|
||||
return Component::focus();
|
||||
}
|
||||
|
||||
void SelectButton::set_selected(bool selected) {
|
||||
if (mProps.selected != selected) {
|
||||
mRoot->SetPseudoClass("selected", selected);
|
||||
mProps.selected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectButton::set_disabled(bool disabled) {
|
||||
if (mProps.disabled != disabled) {
|
||||
if (disabled) {
|
||||
mRoot->SetAttribute("disabled", "");
|
||||
mRoot->SetPseudoClass("disabled", true);
|
||||
mRoot->Blur();
|
||||
} else {
|
||||
mRoot->RemoveAttribute("disabled");
|
||||
mRoot->SetPseudoClass("disabled", false);
|
||||
}
|
||||
mProps.disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectButton::set_value_label(const Rml::String& value) {
|
||||
if (mProps.value != value) {
|
||||
mValueElem->SetInnerRML(escape(value));
|
||||
@@ -78,14 +51,12 @@ void SelectButton::update_props(Props props) {
|
||||
mKeyElem->SetInnerRML(escape(props.key));
|
||||
}
|
||||
set_value_label(props.value);
|
||||
set_selected(props.selected);
|
||||
set_disabled(props.disabled);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
bool SelectButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
set_selected(!get_selected());
|
||||
set_selected(!selected());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -5,25 +5,24 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SelectButton : public Component {
|
||||
class SelectButton : public FluentComponent<SelectButton> {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
Rml::String value;
|
||||
bool selected = false;
|
||||
bool disabled = false;
|
||||
};
|
||||
|
||||
SelectButton(Rml::Element* parent, Props props);
|
||||
|
||||
bool focus() override;
|
||||
|
||||
void set_selected(bool selected);
|
||||
bool get_selected() const { return mProps.selected; }
|
||||
void set_disabled(bool disabled);
|
||||
bool get_disabled() const { return mProps.disabled; }
|
||||
void set_value_label(const Rml::String& value);
|
||||
|
||||
SelectButton& on_selected(std::function<void(bool)> callback) {
|
||||
listen(Rml::EventId::Change, [callback = std::move(callback)](Rml::Event& event) {
|
||||
callback(event.GetParameter("selected", false));
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
protected:
|
||||
void update_props(Props props);
|
||||
virtual bool handle_nav_command(NavCommand cmd);
|
||||
@@ -52,15 +51,10 @@ public:
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<bool()> isDisabled;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
ControlledSelectButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent,
|
||||
BaseControlledSelectButton::Props{
|
||||
.key = std::move(props.key),
|
||||
.selected = props.selected,
|
||||
}),
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}),
|
||||
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)) {}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -12,7 +12,7 @@ Rml::Element* createRoot(Rml::Element* parent) {
|
||||
} // namespace
|
||||
|
||||
TabBar::TabBar(Rml::Element* parent, Props props)
|
||||
: Component(createRoot(parent)), mProps(std::move(props)) {
|
||||
: FluentComponent(createRoot(parent)), mProps(std::move(props)) {
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
|
||||
@@ -43,14 +43,14 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
|
||||
if (selected && callback) {
|
||||
callback();
|
||||
}
|
||||
auto& button = add_child<Button>(Button::Props{title}, "tab");
|
||||
button.on_pressed([this, index] { set_active_tab(index); });
|
||||
if (selected) {
|
||||
button.set_selected(true);
|
||||
}
|
||||
mTabs.emplace_back(Tab{
|
||||
.title = title,
|
||||
.button = add_child<Button>(
|
||||
Button::Props{
|
||||
.text = title,
|
||||
.selected = selected,
|
||||
},
|
||||
"tab").on_pressed([this, index] { set_active_tab(index); }),
|
||||
.button = button,
|
||||
.callback = std::move(callback),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct Tab {
|
||||
TabCallback callback;
|
||||
};
|
||||
|
||||
class TabBar : public Component {
|
||||
class TabBar : public FluentComponent<TabBar> {
|
||||
public:
|
||||
struct Props {
|
||||
std::function<void()> onClose;
|
||||
|
||||
+47
-2
@@ -3,6 +3,7 @@
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "magic_enum.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -70,6 +71,22 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w
|
||||
Document::hide();
|
||||
}
|
||||
});
|
||||
|
||||
// If an item is selected in a pane, focus the next pane in the tree
|
||||
listen(mRoot, Rml::EventId::Change, [this](Rml::Event& event) {
|
||||
if (event.GetParameter("selected", false)) {
|
||||
int paneIndex = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); i++) {
|
||||
if (mContentComponents[i]->contains(event.GetTargetElement())) {
|
||||
paneIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (paneIndex >= 0 && paneIndex < mContentComponents.size() - 1) {
|
||||
mContentComponents[paneIndex + 1]->focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Window::show() {
|
||||
@@ -176,7 +193,27 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
}
|
||||
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Up || cmd == NavCommand::Cancel) {
|
||||
if (cmd == NavCommand::Up) {
|
||||
return focus();
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
int currentComponent = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); ++i) {
|
||||
if (mContentComponents[i]->contains(event.GetTargetElement())) {
|
||||
currentComponent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (; currentComponent > 0; --currentComponent) {
|
||||
if (mContentComponents[currentComponent - 1]->focus()) {
|
||||
// When returning to a previous pane, deselect the item after focusing
|
||||
if (auto* pane =
|
||||
dynamic_cast<Pane*>(mContentComponents[currentComponent - 1].get()))
|
||||
{
|
||||
pane->set_selected_item(-1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return focus();
|
||||
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
int currentComponent = -1;
|
||||
@@ -193,7 +230,15 @@ bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
} else if (i >= 0 && i < mContentComponents.size()) {
|
||||
return mContentComponents[i]->focus();
|
||||
if (mContentComponents[i]->focus()) {
|
||||
if (direction == -1) {
|
||||
// When returning to a previous pane, deselect the item after focusing
|
||||
if (auto* pane = dynamic_cast<Pane*>(mContentComponents[i].get())) {
|
||||
pane->set_selected_item(-1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user