mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-22 05:49:01 -04:00
UiService: Dialog controls (#2332)
This commit is contained in:
+26
-4
@@ -516,10 +516,32 @@ svc_ui->window_push(mod_ctx, &desc, &window);
|
||||
```
|
||||
|
||||
**Dialogs:** `dialog_push` shows a modal dialog. `variant` picks the style, `icon` optionally overrides the variant's
|
||||
default icon, and actions become buttons. After an action's `on_pressed` returns, the dialog closes unless the action
|
||||
sets `keep_open`. A `keep_open` action can close it later (or immediately) with `dialog_close`. Cancel fires
|
||||
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
|
||||
dialog.
|
||||
default icon, and actions become buttons. The optional `build` callback allows you to add controls to a pane between
|
||||
the body and actions. It uses the same text, progress, and control builders as panels.
|
||||
|
||||
```cpp
|
||||
ModResult build_dialog(ModContext*, UiElementHandle pane, void*, ModError*) {
|
||||
UiControlDesc input = UI_CONTROL_DESC_INIT;
|
||||
input.kind = UI_CONTROL_STRING;
|
||||
input.label = "Name";
|
||||
input.get = get_name;
|
||||
input.set = set_name;
|
||||
return svc_ui->pane_add_control(mod_ctx, pane, &input, nullptr);
|
||||
}
|
||||
|
||||
UiDialogAction action = {"Save", save, nullptr, false};
|
||||
UiDialogDesc dialog = UI_DIALOG_DESC_INIT;
|
||||
dialog.title = "New Preset";
|
||||
dialog.body_rml = "Choose a name for the preset.";
|
||||
dialog.actions = &action;
|
||||
dialog.action_count = 1;
|
||||
dialog.build = build_dialog;
|
||||
svc_ui->dialog_push(mod_ctx, &dialog, nullptr);
|
||||
```
|
||||
|
||||
After an action's `on_pressed`, the dialog closes unless the action sets `keep_open`. It can then be closed later
|
||||
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body`, `dialog_set_icon`,
|
||||
and `dialog_add_action` mutate a live dialog.
|
||||
|
||||
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
|
||||
RCSS class; `warning` uses the built-in warning appearance, and mods can define their own types. A duration of 0 uses
|
||||
|
||||
+77
-3
@@ -48,6 +48,12 @@ window.modal {
|
||||
max-width: 640dp;
|
||||
}
|
||||
|
||||
@media (max-height: 896dp) {
|
||||
window.modal {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
window[open] {
|
||||
filter: opacity(1);
|
||||
transform: scale(1);
|
||||
@@ -62,6 +68,16 @@ window[open] {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768dp) {
|
||||
body {
|
||||
padding: 16dp;
|
||||
}
|
||||
window.modal {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
window tab-bar {
|
||||
flex: 0 0 64dp;
|
||||
height: 64dp;
|
||||
@@ -419,14 +435,17 @@ progress {
|
||||
margin: 6dp 0 2dp 0;
|
||||
}
|
||||
|
||||
progress fill {
|
||||
background-color: rgba(194, 164, 45, 80%);
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
progress.progress-done fill {
|
||||
background-color: #44aa22;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
progress.progress-ongoing fill {
|
||||
background-color: #2255bb;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
button.achievement-clear {
|
||||
@@ -466,7 +485,9 @@ button.achievement-clear {
|
||||
padding: 24dp;
|
||||
gap: 20dp;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -507,7 +528,7 @@ window.modal.danger .modal-header icon {
|
||||
.modal-body {
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex: 0 1 auto;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
font-size: 20dp;
|
||||
color: #FFFFFF;
|
||||
@@ -519,6 +540,37 @@ window.modal.danger .modal-header icon {
|
||||
color: #92875B;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-content.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-content pane {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
gap: 8dp;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.modal-content pane > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.modal-content pane > spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.verification-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -563,3 +615,25 @@ progress.verification-progress-bar {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
.modal-dialog {
|
||||
padding: 16dp;
|
||||
gap: 12dp;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
font-size: 17dp;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640dp) {
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-actions button.modal-btn {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||
#define UI_SERVICE_MAJOR 1u
|
||||
#define UI_SERVICE_MINOR 3u
|
||||
#define UI_SERVICE_MINOR 4u
|
||||
|
||||
/*
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
|
||||
@@ -127,11 +127,14 @@ typedef struct UiControlDesc {
|
||||
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
||||
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false}
|
||||
|
||||
/* Build pane contents. A non-MOD_OK result fails the mod. */
|
||||
typedef ModResult (*UiPaneBuildFn)(
|
||||
ModContext* ctx, UiElementHandle pane, void* user_data, ModError* out_error);
|
||||
|
||||
/* Build the panel contents. `panel` accepts the pane_add_* functions; it and
|
||||
* every element created in it are destroyed (handles invalidated) whenever the
|
||||
* panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */
|
||||
typedef ModResult (*UiPanelBuildFn)(
|
||||
ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error);
|
||||
typedef UiPaneBuildFn UiPanelBuildFn;
|
||||
/* Called every frame while the panel is the visible tab. */
|
||||
typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error);
|
||||
|
||||
@@ -147,8 +150,7 @@ typedef struct UiModsPanelDesc {
|
||||
|
||||
/* Builds the contents associated with a group button. The target pane is cleared immediately
|
||||
* before this callback and is valid only while its tab remains built. */
|
||||
typedef ModResult (*UiGroupBuildFn)(
|
||||
ModContext* ctx, UiElementHandle target_pane, void* user_data, ModError* out_error);
|
||||
typedef UiPaneBuildFn UiGroupBuildFn;
|
||||
|
||||
typedef struct UiGroupDesc {
|
||||
uint32_t struct_size;
|
||||
@@ -216,11 +218,15 @@ typedef struct UiDialogDesc {
|
||||
/* Fired on cancel (B/Escape) before the dialog closes; the dialog always
|
||||
* closes on dismiss. */
|
||||
UiDialogActionFn on_dismiss;
|
||||
void* user_data; /* passed to on_dismiss */
|
||||
void* user_data; /* passed to build and on_dismiss */
|
||||
/* Optional content builder. The pane is rendered below body_rml and above the actions. Its
|
||||
* handle and all child element handles remain valid until the dialog closes.
|
||||
* Added in UiService minor version 4. */
|
||||
UiPaneBuildFn build;
|
||||
} UiDialogDesc;
|
||||
|
||||
#define UI_DIALOG_DESC_INIT \
|
||||
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL}
|
||||
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL, NULL}
|
||||
|
||||
/* A tab added to the in-game menu bar. */
|
||||
typedef struct UiMenuTabDesc {
|
||||
|
||||
@@ -808,7 +808,8 @@ ModResult ui_window_close(LoadedMod& mod, uint64_t handle) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& outHandle) {
|
||||
ModResult ui_dialog_push(
|
||||
LoadedMod& mod, const UiDialogDesc& desc, UiPaneBuildFn build, uint64_t& outHandle) {
|
||||
outHandle = 0;
|
||||
if (!aurora::rmlui::is_initialized()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
@@ -851,6 +852,16 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out
|
||||
|
||||
auto dialog = std::make_unique<ModDialog>(
|
||||
std::move(props), [handle] { on_mod_dialog_destroyed(handle); });
|
||||
if (build != nullptr) {
|
||||
auto& pane = dialog->content_pane();
|
||||
const uint64_t paneHandle = wrap_pane(mod, pane, nullptr);
|
||||
invoke_mod_ui_callback(mod, "mod UI dialog build", [&](ModError* error) {
|
||||
return build(mod.context.get(), paneHandle, desc.user_data, error);
|
||||
});
|
||||
if (!mod.active) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
if (auto* slot = slot_from_handle(handle)) {
|
||||
slot->document = dialog.get();
|
||||
}
|
||||
@@ -1338,7 +1349,8 @@ ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialog
|
||||
*outDialog = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiDialogDesc) ||
|
||||
constexpr size_t kLegacyDescSize = offsetof(UiDialogDesc, build);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < kLegacyDescSize ||
|
||||
desc->title == nullptr || desc->body_rml == nullptr || desc->actions == nullptr ||
|
||||
desc->action_count == 0 || desc->variant > UI_DIALOG_DANGER)
|
||||
{
|
||||
@@ -1349,8 +1361,9 @@ ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialog
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
const UiPaneBuildFn build = desc->struct_size >= sizeof(UiDialogDesc) ? desc->build : nullptr;
|
||||
uint64_t handle = 0;
|
||||
const auto result = ui_impl::ui_dialog_push(*mod, *desc, handle);
|
||||
const auto result = ui_impl::ui_dialog_push(*mod, *desc, build, handle);
|
||||
if (result == MOD_OK && outDialog != nullptr) {
|
||||
*outDialog = handle;
|
||||
}
|
||||
|
||||
+49
-2
@@ -1,5 +1,7 @@
|
||||
#include "modal.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
|
||||
@@ -23,6 +25,9 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
|
||||
body->SetClass("modal-body", true);
|
||||
body->SetInnerRML(mProps.bodyRml);
|
||||
|
||||
mContentRoot = append(mDialog, "div");
|
||||
mContentRoot->SetClass("modal-content", true);
|
||||
|
||||
auto* actions = append(mDialog, "div");
|
||||
actions->SetClass("modal-actions", true);
|
||||
if (props.isVertical) {
|
||||
@@ -36,12 +41,36 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
|
||||
mDoAud_seStartMenu(kSoundWindowOpen);
|
||||
}
|
||||
|
||||
void Modal::update() {
|
||||
if (mContentPane != nullptr) {
|
||||
mContentPane->update();
|
||||
}
|
||||
if (mPendingAction) {
|
||||
auto action = std::move(mPendingAction);
|
||||
action(*this);
|
||||
}
|
||||
WindowSmall::update();
|
||||
}
|
||||
|
||||
Pane& Modal::content_pane() {
|
||||
if (mContentPane == nullptr) {
|
||||
mContentRoot->SetClass("active", true);
|
||||
mContentPane = std::make_unique<Pane>(mContentRoot, Pane::Type::Uncontrolled);
|
||||
}
|
||||
return *mContentPane;
|
||||
}
|
||||
|
||||
void Modal::add_action(ModalAction action) {
|
||||
auto* actions = mDialog->QuerySelector(".modal-actions");
|
||||
auto btn = std::make_unique<Button>(actions, action.label);
|
||||
btn->root()->SetClass("modal-btn", true);
|
||||
btn->on_pressed([this, callback = std::move(action.onPressed)] {
|
||||
if (callback) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
if (mContentPane != nullptr) {
|
||||
mPendingAction = callback;
|
||||
} else {
|
||||
callback(*this);
|
||||
}
|
||||
});
|
||||
@@ -68,6 +97,9 @@ void Modal::set_icon(const Rml::String& icon) {
|
||||
}
|
||||
|
||||
bool Modal::focus() {
|
||||
if (mContentPane != nullptr && mContentPane->focus()) {
|
||||
return true;
|
||||
}
|
||||
if (!mButtons.empty()) {
|
||||
return mButtons.front()->focus();
|
||||
}
|
||||
@@ -89,6 +121,22 @@ bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* target = event.GetTargetElement();
|
||||
if (mContentPane != nullptr && mContentPane->contains(target) && cmd == NavCommand::Down &&
|
||||
!mButtons.empty() && mButtons.front()->focus())
|
||||
{
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
return true;
|
||||
}
|
||||
if (mContentPane != nullptr && cmd == NavCommand::Up &&
|
||||
std::ranges::any_of(
|
||||
mButtons, [target](const auto& button) { return button->contains(target); }) &&
|
||||
mContentPane->focus_last())
|
||||
{
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
return true;
|
||||
}
|
||||
|
||||
int direction = 0;
|
||||
NavCommand prevCommand = mProps.isVertical ? NavCommand::Up : NavCommand::Left;
|
||||
NavCommand nextCommand = mProps.isVertical ? NavCommand::Down : NavCommand::Right;
|
||||
@@ -100,7 +148,6 @@ bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* target = event.GetTargetElement();
|
||||
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
|
||||
if (mButtons[i]->contains(target)) {
|
||||
const int next = i + direction;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -20,13 +21,15 @@ public:
|
||||
std::function<void(Modal&)> onDismiss;
|
||||
Rml::String variant;
|
||||
Rml::String icon = "";
|
||||
bool isVertical;
|
||||
bool isVertical = false;
|
||||
};
|
||||
|
||||
explicit Modal(Props props);
|
||||
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
|
||||
Pane& content_pane();
|
||||
void add_action(ModalAction action);
|
||||
void set_body(const Rml::String& bodyRml);
|
||||
void set_icon(const Rml::String& icon);
|
||||
@@ -38,7 +41,10 @@ private:
|
||||
void dismiss();
|
||||
|
||||
Props mProps;
|
||||
std::vector<std::unique_ptr<Button> > mButtons;
|
||||
Rml::Element* mContentRoot = nullptr;
|
||||
std::unique_ptr<Pane> mContentPane;
|
||||
std::function<void(Modal&)> mPendingAction;
|
||||
std::vector<std::unique_ptr<Button>> mButtons;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -163,6 +163,15 @@ bool Pane::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pane::focus_last() {
|
||||
for (auto child = mChildren.rbegin(); child != mChildren.rend(); ++child) {
|
||||
if ((*child)->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetClass("section-heading", true);
|
||||
|
||||
@@ -17,6 +17,7 @@ public:
|
||||
explicit Pane(Rml::Element* parent, Type type);
|
||||
|
||||
bool focus() override;
|
||||
bool focus_last();
|
||||
void update() override;
|
||||
|
||||
void set_selected_item(int index);
|
||||
|
||||
@@ -52,6 +52,10 @@ Popover::Popover(Rml::Element* anchor, Side side, const Rml::String& windowClass
|
||||
true);
|
||||
}
|
||||
|
||||
Popover::~Popover() {
|
||||
notify_close(false);
|
||||
}
|
||||
|
||||
void Popover::show() {
|
||||
Document::show();
|
||||
reposition();
|
||||
@@ -60,7 +64,7 @@ void Popover::show() {
|
||||
}
|
||||
|
||||
void Popover::hide(bool close) {
|
||||
notify_close();
|
||||
notify_close(true);
|
||||
mBody->RemoveAttribute("open");
|
||||
mPendingClose = close;
|
||||
}
|
||||
@@ -136,13 +140,13 @@ void Popover::reposition() {
|
||||
mBody->SetProperty(Rml::PropertyId::Top, Rml::Property{pos.y, Rml::Unit::PX});
|
||||
}
|
||||
|
||||
void Popover::notify_close() {
|
||||
void Popover::notify_close(bool restoreFocus) {
|
||||
if (mOnClose) {
|
||||
auto callback = std::move(mOnClose);
|
||||
mOnClose = nullptr;
|
||||
callback();
|
||||
}
|
||||
if (mAnchor != nullptr && mAnchor->IsVisible(true)) {
|
||||
if (restoreFocus && mAnchor != nullptr && mAnchor->IsVisible(true)) {
|
||||
mAnchor->Focus(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public:
|
||||
};
|
||||
|
||||
Popover(Rml::Element* anchor, Side side, const Rml::String& windowClass = "");
|
||||
~Popover() override;
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
@@ -32,7 +33,7 @@ protected:
|
||||
|
||||
private:
|
||||
void reposition();
|
||||
void notify_close();
|
||||
void notify_close(bool restoreFocus);
|
||||
|
||||
Rml::Element* mAnchor = nullptr;
|
||||
Side mSide;
|
||||
|
||||
Reference in New Issue
Block a user