Dual pane navigation & more player editor

This commit is contained in:
Luke Street
2026-05-01 12:06:00 -06:00
parent 2b505f1be4
commit 8f7b9cdfdd
13 changed files with 437 additions and 115 deletions
+10 -11
View File
@@ -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
+3 -4
View File
@@ -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;
+28 -1
View File
@@ -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) {
+28 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4 -5
View File
@@ -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) {
+7 -36
View File
@@ -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;
+9 -15
View File
@@ -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:
+7 -7
View File
@@ -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),
});
}
+1 -1
View File
@@ -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
View File
@@ -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;